securenow 5.18.0 → 6.0.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/LICENSE +15 -0
- package/README.md +40 -239
- package/cli.js +455 -415
- package/console-instrumentation.js +136 -147
- package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-BODY-CAPTURE.md +1 -1
- package/docs/AUTO-SETUP.md +4 -4
- package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
- package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
- package/docs/CHANGELOG-NEXTJS.md +1 -1
- package/docs/CUSTOMER-GUIDE.md +16 -16
- package/docs/EASIEST-SETUP.md +5 -5
- package/docs/ENVIRONMENT-VARIABLES.md +652 -880
- package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
- package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
- package/docs/INDEX.md +4 -22
- package/docs/LOGGING-GUIDE.md +708 -701
- package/docs/LOGGING-QUICKSTART.md +239 -234
- 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-WRAPPER-APPROACH.md +1 -1
- package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
- package/docs/REDACTION-EXAMPLES.md +1 -1
- package/docs/REQUEST-BODY-CAPTURE.md +10 -19
- 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/nextjs-auto-capture.js +207 -199
- package/nextjs-middleware.js +181 -186
- package/nextjs-webpack-config.js +53 -88
- package/nextjs-wrapper.js +158 -158
- package/nextjs.d.ts +1 -1
- package/nextjs.js +135 -190
- package/package.json +45 -67
- package/postinstall.js +6 -6
- package/register.d.ts +1 -1
- package/register.js +4 -39
- package/tracing.d.ts +1 -2
- package/tracing.js +22 -287
- package/web-vite.mjs +156 -239
- package/CONSUMING-APPS-GUIDE.md +0 -455
- package/NPM_README.md +0 -1933
- package/SKILL-API.md +0 -600
- package/SKILL-CLI.md +0 -409
- package/cidr.js +0 -83
- package/cli/apps.js +0 -585
- package/cli/auth.js +0 -280
- package/cli/client.js +0 -115
- package/cli/config.js +0 -173
- package/cli/firewall.js +0 -100
- package/cli/fp.js +0 -638
- package/cli/init.js +0 -201
- package/cli/monitor.js +0 -440
- package/cli/run.js +0 -133
- package/cli/security.js +0 -1064
- package/cli/ui.js +0 -386
- package/docs/API-KEYS-GUIDE.md +0 -233
- package/docs/AUTO-SETUP-SUMMARY.md +0 -331
- package/docs/BODY-CAPTURE-FIX.md +0 -261
- package/docs/COMPLETION-REPORT.md +0 -408
- package/docs/FINAL-SOLUTION.md +0 -335
- package/docs/FIREWALL-GUIDE.md +0 -426
- package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
- package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
- package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
- package/docs/NUXT-GUIDE.md +0 -166
- package/docs/SOLUTION-SUMMARY.md +0 -312
- package/firewall-cloud.js +0 -212
- package/firewall-iptables.js +0 -139
- package/firewall-only.js +0 -38
- package/firewall-tcp.js +0 -74
- package/firewall.js +0 -720
- package/free-trial-banner.js +0 -174
- package/nuxt-server-plugin.mjs +0 -423
- package/nuxt.d.ts +0 -60
- package/nuxt.mjs +0 -75
- package/resolve-ip.js +0 -77
package/register.js
CHANGED
|
@@ -1,49 +1,14 @@
|
|
|
1
|
-
// securenow/
|
|
2
|
-
// node --require securenow/register app.js
|
|
3
|
-
//
|
|
4
|
-
// For ESM apps ("type": "module"), this file auto-registers the
|
|
5
|
-
// OpenTelemetry ESM loader hook via module.register() (Node >=20.6).
|
|
6
|
-
// On older Node versions it falls back to a warning.
|
|
1
|
+
// securenow/preload.js
|
|
7
2
|
'use strict';
|
|
8
3
|
|
|
9
|
-
//
|
|
4
|
+
// load .env into process.env before anything else
|
|
10
5
|
try {
|
|
11
6
|
require('dotenv').config();
|
|
12
7
|
console.log('[securenow] dotenv loaded from', process.env.DOTENV_CONFIG_PATH || '.env');
|
|
13
8
|
} catch (e) {
|
|
9
|
+
// dotenv is optional — only warn if it’s missing
|
|
14
10
|
console.warn('[securenow] dotenv not found or failed to load');
|
|
15
11
|
}
|
|
16
12
|
|
|
17
|
-
//
|
|
18
|
-
(() => {
|
|
19
|
-
try {
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
23
|
-
if (!fs.existsSync(pkgPath)) return;
|
|
24
|
-
|
|
25
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
26
|
-
if (pkg.type !== 'module') return;
|
|
27
|
-
|
|
28
|
-
// Already registered via --import?
|
|
29
|
-
const execArgv = process.execArgv.join(' ');
|
|
30
|
-
if (execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle')) return;
|
|
31
|
-
|
|
32
|
-
// Node >=20.6 exposes module.register() for programmatic ESM hooks
|
|
33
|
-
const mod = require('node:module');
|
|
34
|
-
if (typeof mod.register !== 'function') {
|
|
35
|
-
console.warn('[securenow] ESM app detected but Node %s lacks module.register().', process.version);
|
|
36
|
-
console.warn('[securenow] Upgrade to Node >=20.6 or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const { pathToFileURL } = require('node:url');
|
|
41
|
-
mod.register('@opentelemetry/instrumentation/hook.mjs', pathToFileURL(__filename));
|
|
42
|
-
console.log('[securenow] ESM loader hook auto-registered (module.register)');
|
|
43
|
-
} catch (_) {
|
|
44
|
-
// Non-fatal — tracing.js will show its own ESM warning if the hook is missing
|
|
45
|
-
}
|
|
46
|
-
})();
|
|
47
|
-
|
|
48
|
-
// 3. Run the OTel SDK setup
|
|
13
|
+
// then run the real tracer preload
|
|
49
14
|
require('./tracing');
|
package/tracing.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export interface LoggerProvider {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Get a logger instance for sending structured logs to
|
|
115
|
+
* Get a logger instance for sending structured logs to SigNoz
|
|
116
116
|
*
|
|
117
117
|
* @param name - Logger name (e.g., 'my-service', 'auth-module')
|
|
118
118
|
* @param version - Logger version (optional, defaults to '1.0.0')
|
|
@@ -178,6 +178,5 @@ export const loggerProvider: LoggerProvider | null;
|
|
|
178
178
|
* - SECURENOW_DISABLE_INSTRUMENTATIONS=pkg1,pkg2
|
|
179
179
|
* - OTEL_LOG_LEVEL=info|debug
|
|
180
180
|
* - SECURENOW_TEST_SPAN=1
|
|
181
|
-
* - SECURENOW_TRUSTED_PROXIES=ip1,ip2 # Additional trusted proxy IPs for X-Forwarded-For
|
|
182
181
|
* - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=... # Override logs endpoint
|
|
183
182
|
*/
|
package/tracing.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Preload with:
|
|
5
|
-
*
|
|
6
|
-
* Works for both CJS and ESM apps. On Node >=20.6 the ESM loader hook is
|
|
7
|
-
* auto-registered via module.register() — no --import flag needed.
|
|
8
|
-
* On Node 18 with "type": "module", add the hook manually:
|
|
9
|
-
* node --import @opentelemetry/instrumentation/hook.mjs --require securenow/register app.js
|
|
4
|
+
* Preload with: NODE_OPTIONS="-r securenow/register"
|
|
10
5
|
*
|
|
11
6
|
* Env:
|
|
12
7
|
* SECURENOW_APPID=logical-name # or OTEL_SERVICE_NAME=logical-name
|
|
@@ -16,7 +11,6 @@
|
|
|
16
11
|
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
17
12
|
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
18
13
|
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
19
|
-
* SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
|
|
20
14
|
* OTEL_LOG_LEVEL=info|debug
|
|
21
15
|
* SECURENOW_TEST_SPAN=1
|
|
22
16
|
*
|
|
@@ -24,7 +18,7 @@
|
|
|
24
18
|
* SECURENOW_STRICT=1 -> if no appid/name is provided in cluster, exit(1) so PM2 restarts the worker
|
|
25
19
|
*/
|
|
26
20
|
|
|
27
|
-
const { diag, DiagConsoleLogger, DiagLogLevel
|
|
21
|
+
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
|
|
28
22
|
const { NodeSDK } = require('@opentelemetry/sdk-node');
|
|
29
23
|
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
|
|
30
24
|
const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-http');
|
|
@@ -32,7 +26,6 @@ const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-
|
|
|
32
26
|
const { Resource } = require('@opentelemetry/resources');
|
|
33
27
|
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
34
28
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
35
|
-
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
36
29
|
const { v4: uuidv4 } = require('uuid');
|
|
37
30
|
|
|
38
31
|
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
@@ -53,10 +46,6 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
53
46
|
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
54
47
|
];
|
|
55
48
|
|
|
56
|
-
function escapeRegex(str) {
|
|
57
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
49
|
/**
|
|
61
50
|
* Redact sensitive fields from an object
|
|
62
51
|
*/
|
|
@@ -65,7 +54,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
65
54
|
|
|
66
55
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
67
56
|
|
|
68
|
-
for (const key
|
|
57
|
+
for (const key in redacted) {
|
|
69
58
|
const lowerKey = key.toLowerCase();
|
|
70
59
|
|
|
71
60
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -88,10 +77,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
88
77
|
|
|
89
78
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
90
79
|
sensitiveFields.forEach(field => {
|
|
91
|
-
|
|
80
|
+
// Match patterns: field: "value" or field: 'value' or field:"value"
|
|
92
81
|
const patterns = [
|
|
93
|
-
new RegExp(`(${
|
|
94
|
-
new RegExp(`(${
|
|
82
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
83
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
95
84
|
];
|
|
96
85
|
|
|
97
86
|
patterns.forEach(pattern => {
|
|
@@ -108,155 +97,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
108
97
|
return redacted;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
// -------- Multipart streaming parser --------
|
|
112
|
-
// Streams through the request without buffering file content.
|
|
113
|
-
// Only part headers and text-field values are kept in memory,
|
|
114
|
-
// so memory stays bounded (~few KB) regardless of upload size.
|
|
115
|
-
|
|
116
|
-
function extractBoundary(contentType) {
|
|
117
|
-
const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
|
|
118
|
-
return match ? (match[1] || match[2]) : null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
122
|
-
const boundary = extractBoundary(contentType);
|
|
123
|
-
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
|
|
124
|
-
|
|
125
|
-
const result = { fields: Object.create(null), files: [] };
|
|
126
|
-
let totalSize = 0;
|
|
127
|
-
let buf = Buffer.alloc(0);
|
|
128
|
-
|
|
129
|
-
const MAX_PARTS = 100;
|
|
130
|
-
let partCount = 0;
|
|
131
|
-
|
|
132
|
-
const FIRST_DELIM = Buffer.from('--' + boundary);
|
|
133
|
-
const DELIM = Buffer.from('\r\n--' + boundary);
|
|
134
|
-
const HDR_END = Buffer.from('\r\n\r\n');
|
|
135
|
-
|
|
136
|
-
let initialized = false;
|
|
137
|
-
let inHeaders = true;
|
|
138
|
-
let isFile = false;
|
|
139
|
-
let fldName = '';
|
|
140
|
-
let fName = '';
|
|
141
|
-
let pCT = '';
|
|
142
|
-
let bodyBytes = 0;
|
|
143
|
-
let textVal = '';
|
|
144
|
-
|
|
145
|
-
function flushPart() {
|
|
146
|
-
if (!fldName || fldName === '__proto__' || fldName === 'constructor' || fldName === 'prototype') return;
|
|
147
|
-
if (isFile) {
|
|
148
|
-
result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
|
|
149
|
-
} else {
|
|
150
|
-
const lower = fldName.toLowerCase();
|
|
151
|
-
const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
|
|
152
|
-
result.fields[fldName] = redact ? '[REDACTED]' : textVal.substring(0, maxTextFieldSize);
|
|
153
|
-
}
|
|
154
|
-
fldName = ''; bodyBytes = 0; textVal = ''; partCount++;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function drain() {
|
|
158
|
-
if (!initialized) {
|
|
159
|
-
const i = buf.indexOf(FIRST_DELIM);
|
|
160
|
-
if (i === -1) {
|
|
161
|
-
if (buf.length > FIRST_DELIM.length + 4) buf = buf.slice(buf.length - FIRST_DELIM.length - 4);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
buf = buf.slice(i + FIRST_DELIM.length);
|
|
165
|
-
initialized = true;
|
|
166
|
-
inHeaders = true;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let guard = 200;
|
|
170
|
-
while (buf.length > 0 && guard-- > 0 && partCount < MAX_PARTS) {
|
|
171
|
-
if (inHeaders) {
|
|
172
|
-
if (buf.length >= 2 && buf[0] === 0x2D && buf[1] === 0x2D) { buf = Buffer.alloc(0); return; }
|
|
173
|
-
if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) { buf = buf.slice(2); continue; }
|
|
174
|
-
|
|
175
|
-
const hi = buf.indexOf(HDR_END);
|
|
176
|
-
if (hi === -1) return;
|
|
177
|
-
|
|
178
|
-
const hdr = buf.slice(0, hi).toString('latin1');
|
|
179
|
-
buf = buf.slice(hi + 4);
|
|
180
|
-
|
|
181
|
-
const nm = hdr.match(/name="([^"]+)"/);
|
|
182
|
-
const fn = hdr.match(/filename="([^"]*)"/);
|
|
183
|
-
const ct = hdr.match(/Content-Type:\s*(.+)/i);
|
|
184
|
-
fldName = nm ? nm[1] : '';
|
|
185
|
-
fName = fn ? fn[1] : '';
|
|
186
|
-
pCT = ct ? ct[1].trim() : '';
|
|
187
|
-
isFile = !!fn;
|
|
188
|
-
bodyBytes = 0;
|
|
189
|
-
textVal = '';
|
|
190
|
-
inHeaders = false;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const di = buf.indexOf(DELIM);
|
|
194
|
-
if (di === -1) {
|
|
195
|
-
const safe = Math.max(0, buf.length - DELIM.length - 2);
|
|
196
|
-
if (safe > 0) {
|
|
197
|
-
bodyBytes += safe;
|
|
198
|
-
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
199
|
-
textVal += buf.slice(0, safe).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
200
|
-
}
|
|
201
|
-
buf = buf.slice(safe);
|
|
202
|
-
}
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
bodyBytes += di;
|
|
207
|
-
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
208
|
-
textVal += buf.slice(0, di).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
209
|
-
}
|
|
210
|
-
flushPart();
|
|
211
|
-
buf = buf.slice(di + DELIM.length);
|
|
212
|
-
inHeaders = true;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
request.on('data', (chunk) => {
|
|
217
|
-
totalSize += chunk.length;
|
|
218
|
-
buf = Buffer.concat([buf, chunk]);
|
|
219
|
-
drain();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
request.on('end', () => {
|
|
223
|
-
try {
|
|
224
|
-
if (!inHeaders && fldName) {
|
|
225
|
-
bodyBytes += buf.length;
|
|
226
|
-
if (!isFile) textVal += buf.toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
227
|
-
flushPart();
|
|
228
|
-
}
|
|
229
|
-
onComplete({ parsed: result, totalSize });
|
|
230
|
-
} catch (e) {
|
|
231
|
-
onComplete({ error: 'PARSE_ERROR' });
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// -------- ESM detection --------
|
|
237
|
-
// register.js auto-registers the hook via module.register() on Node >=20.6.
|
|
238
|
-
// This warning only fires if BOTH --import AND module.register() were skipped
|
|
239
|
-
// (e.g. Node 18, or require('securenow/tracing') called directly without register.js).
|
|
240
|
-
(() => {
|
|
241
|
-
try {
|
|
242
|
-
const fs = require('fs');
|
|
243
|
-
const path = require('path');
|
|
244
|
-
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
245
|
-
if (fs.existsSync(pkgPath)) {
|
|
246
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
247
|
-
if (pkg.type === 'module') {
|
|
248
|
-
const execArgv = process.execArgv.join(' ');
|
|
249
|
-
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
250
|
-
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
251
|
-
if (!hasCliHook && !hasModuleRegister) {
|
|
252
|
-
console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
253
|
-
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
} catch (_) {}
|
|
258
|
-
})();
|
|
259
|
-
|
|
260
100
|
// -------- diagnostics --------
|
|
261
101
|
(() => {
|
|
262
102
|
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
@@ -320,44 +160,21 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
320
160
|
|
|
321
161
|
// -------- Body Capture Configuration --------
|
|
322
162
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
323
|
-
const maxBodySize =
|
|
163
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
324
164
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
325
165
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
326
166
|
|
|
327
|
-
const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || String(env('SECURENOW_CAPTURE_MULTIPART')).toLowerCase() === 'true';
|
|
328
|
-
|
|
329
|
-
// -------- Trusted proxy IP resolution --------
|
|
330
|
-
const { resolveClientIp, isFromTrustedProxy, LOOPBACK_RE } = require('./resolve-ip');
|
|
331
|
-
|
|
332
167
|
// Configure HTTP instrumentation with body capture
|
|
333
168
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
334
169
|
const httpInstrumentation = new HttpInstrumentation({
|
|
335
170
|
requestHook: (span, request) => {
|
|
336
171
|
try {
|
|
337
|
-
|
|
338
|
-
if (clientIp) {
|
|
339
|
-
span.setAttribute('http.client_ip', clientIp);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (request.headers) {
|
|
343
|
-
const SKIP_HEADERS = new Set(['cookie', 'authorization', 'proxy-authorization', 'set-cookie', 'x-api-key', 'x-auth-token']);
|
|
344
|
-
const safe = {};
|
|
345
|
-
for (const [k, v] of Object.entries(request.headers)) {
|
|
346
|
-
if (SKIP_HEADERS.has(k.toLowerCase())) { safe[k] = '[REDACTED]'; continue; }
|
|
347
|
-
safe[k] = typeof v === 'string' ? v.substring(0, 500) : String(v);
|
|
348
|
-
}
|
|
349
|
-
const serialized = JSON.stringify(safe);
|
|
350
|
-
if (serialized.length <= 8192) {
|
|
351
|
-
span.setAttribute('http.request.headers', serialized);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if ((captureBody || captureMultipart) && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
172
|
+
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
356
173
|
const contentType = request.headers['content-type'] || '';
|
|
357
174
|
|
|
358
|
-
if (
|
|
175
|
+
if (contentType.includes('application/json') ||
|
|
359
176
|
contentType.includes('application/graphql') ||
|
|
360
|
-
contentType.includes('application/x-www-form-urlencoded'))
|
|
177
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
361
178
|
|
|
362
179
|
let body = '';
|
|
363
180
|
const chunks = [];
|
|
@@ -405,9 +222,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
405
222
|
});
|
|
406
223
|
}
|
|
407
224
|
} catch (e) {
|
|
408
|
-
|
|
225
|
+
// Parse error: capture as-is (truncated)
|
|
226
|
+
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
409
227
|
span.setAttribute('http.request.body.parse_error', true);
|
|
410
|
-
span.setAttribute('http.request.body.size', size);
|
|
411
228
|
}
|
|
412
229
|
} else if (size > maxBodySize) {
|
|
413
230
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
@@ -415,38 +232,10 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
415
232
|
}
|
|
416
233
|
});
|
|
417
234
|
} else if (contentType.includes('multipart/form-data')) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
423
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (error) {
|
|
427
|
-
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
428
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
429
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
span.setAttributes({
|
|
433
|
-
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
434
|
-
'http.request.body.type': 'multipart',
|
|
435
|
-
'http.request.body.size': totalSize,
|
|
436
|
-
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
437
|
-
'http.request.body.files_count': parsed.files.length,
|
|
438
|
-
});
|
|
439
|
-
} catch (e) {
|
|
440
|
-
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
441
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
442
|
-
span.setAttribute('http.request.body.parse_error', true);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
} else {
|
|
446
|
-
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
447
|
-
span.setAttribute('http.request.body.type', 'multipart');
|
|
448
|
-
span.setAttribute('http.request.body.note', 'Set SECURENOW_CAPTURE_MULTIPART=1 to enable');
|
|
449
|
-
}
|
|
235
|
+
// Multipart is NOT captured
|
|
236
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
237
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
238
|
+
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
450
239
|
}
|
|
451
240
|
}
|
|
452
241
|
} catch (error) {
|
|
@@ -456,7 +245,7 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
456
245
|
});
|
|
457
246
|
|
|
458
247
|
// -------- Logging Configuration --------
|
|
459
|
-
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED'))
|
|
248
|
+
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) !== '0' && String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() !== 'false';
|
|
460
249
|
|
|
461
250
|
// Create shared resource for both traces and logs
|
|
462
251
|
const sharedResource = new Resource({
|
|
@@ -468,6 +257,7 @@ const sharedResource = new Resource({
|
|
|
468
257
|
|
|
469
258
|
// Initialize LoggerProvider if logging is enabled
|
|
470
259
|
let loggerProvider = null;
|
|
260
|
+
let globalLogger = null;
|
|
471
261
|
|
|
472
262
|
if (loggingEnabled) {
|
|
473
263
|
const logExporter = new OTLPLogExporter({
|
|
@@ -475,35 +265,12 @@ if (loggingEnabled) {
|
|
|
475
265
|
headers
|
|
476
266
|
});
|
|
477
267
|
|
|
478
|
-
const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
|
|
479
268
|
loggerProvider = new LoggerProvider({
|
|
480
269
|
resource: sharedResource,
|
|
270
|
+
processors: [new BatchLogRecordProcessor(logExporter)],
|
|
481
271
|
});
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// Auto-patch console.* so every log/warn/error becomes an OTel log record
|
|
485
|
-
const _logger = loggerProvider.getLogger('console', '1.0.0');
|
|
486
|
-
const _orig = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug };
|
|
487
|
-
const SEV = { DEBUG: 5, INFO: 9, WARN: 13, ERROR: 17 };
|
|
488
|
-
function _emit(sn, st, args) {
|
|
489
|
-
try {
|
|
490
|
-
const activeCtx = context.active();
|
|
491
|
-
const spanCtx = trace.getSpanContext(activeCtx);
|
|
492
|
-
_logger.emit({
|
|
493
|
-
severityNumber: sn,
|
|
494
|
-
severityText: st,
|
|
495
|
-
body: args.map(a => (typeof a === 'object' && a !== null) ? JSON.stringify(a) : String(a)).join(' '),
|
|
496
|
-
attributes: { 'log.source': 'console', 'log.method': st.toLowerCase() },
|
|
497
|
-
...(spanCtx && { context: activeCtx }),
|
|
498
|
-
});
|
|
499
|
-
} catch (_) {}
|
|
500
|
-
}
|
|
501
|
-
console.log = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.log.apply(console, a); };
|
|
502
|
-
console.info = function (...a) { _emit(SEV.INFO, 'INFO', a); _orig.info.apply(console, a); };
|
|
503
|
-
console.warn = function (...a) { _emit(SEV.WARN, 'WARN', a); _orig.warn.apply(console, a); };
|
|
504
|
-
console.error = function (...a) { _emit(SEV.ERROR, 'ERROR', a); _orig.error.apply(console, a); };
|
|
505
|
-
console.debug = function (...a) { _emit(SEV.DEBUG, 'DEBUG', a); _orig.debug.apply(console, a); };
|
|
506
|
-
console.__securenow_patched = true;
|
|
272
|
+
|
|
273
|
+
globalLogger = loggerProvider.getLogger('securenow', '1.0.0');
|
|
507
274
|
}
|
|
508
275
|
|
|
509
276
|
// -------- SDK --------
|
|
@@ -512,11 +279,9 @@ const sdk = new NodeSDK({
|
|
|
512
279
|
traceExporter,
|
|
513
280
|
instrumentations: [
|
|
514
281
|
httpInstrumentation,
|
|
515
|
-
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
516
282
|
...getNodeAutoInstrumentations({
|
|
517
283
|
...disabledMap,
|
|
518
|
-
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
519
|
-
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
284
|
+
'@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
|
|
520
285
|
}),
|
|
521
286
|
],
|
|
522
287
|
resource: sharedResource,
|
|
@@ -535,52 +300,22 @@ const sdk = new NodeSDK({
|
|
|
535
300
|
if (captureBody) {
|
|
536
301
|
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
537
302
|
}
|
|
538
|
-
if (captureMultipart) {
|
|
539
|
-
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
540
|
-
}
|
|
541
303
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
542
304
|
const api = require('@opentelemetry/api');
|
|
543
305
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
544
306
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
545
307
|
}
|
|
546
|
-
|
|
547
|
-
// Free trial banner
|
|
548
|
-
const { isFreeTrial, patchHttpForBanner } = require('./free-trial-banner');
|
|
549
|
-
if (isFreeTrial(endpointBase) && String(env('SECURENOW_HIDE_BANNER')) !== '1') {
|
|
550
|
-
patchHttpForBanner();
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Firewall — auto-activates when SECURENOW_API_KEY is set
|
|
554
|
-
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
555
|
-
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
556
|
-
require('./firewall').init({
|
|
557
|
-
apiKey: firewallApiKey,
|
|
558
|
-
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
559
|
-
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
560
|
-
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
|
561
|
-
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
562
|
-
statusCode: parseInt(env('SECURENOW_FIREWALL_STATUS_CODE'), 10) || 403,
|
|
563
|
-
log: env('SECURENOW_FIREWALL_LOG') !== '0',
|
|
564
|
-
tcp: env('SECURENOW_FIREWALL_TCP') === '1',
|
|
565
|
-
iptables: env('SECURENOW_FIREWALL_IPTABLES') === '1',
|
|
566
|
-
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
308
|
} catch (e) {
|
|
570
309
|
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
571
310
|
}
|
|
572
311
|
})();
|
|
573
312
|
|
|
574
|
-
let shuttingDown = false;
|
|
575
313
|
async function safeShutdown(sig) {
|
|
576
|
-
if (shuttingDown) return;
|
|
577
|
-
shuttingDown = true;
|
|
578
314
|
try {
|
|
579
315
|
await Promise.resolve(sdk.shutdown?.());
|
|
580
316
|
if (loggerProvider) {
|
|
581
317
|
await Promise.resolve(loggerProvider.shutdown?.());
|
|
582
318
|
}
|
|
583
|
-
try { require('./firewall').shutdown(); } catch (_) {}
|
|
584
319
|
console.log(`[securenow] Tracing and logging terminated on ${sig}`);
|
|
585
320
|
}
|
|
586
321
|
catch (e) { console.error('[securenow] Shutdown error:', e); }
|