securenow 5.18.0 → 6.0.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/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 +35 -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 +255 -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 +198 -186
- 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 +26 -286
- 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/postinstall.js
CHANGED
|
@@ -53,7 +53,7 @@ export function register() {
|
|
|
53
53
|
* SECURENOW_APPID=my-nextjs-app
|
|
54
54
|
*
|
|
55
55
|
* Optional:
|
|
56
|
-
* SECURENOW_INSTANCE=http://your-
|
|
56
|
+
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
57
57
|
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
58
58
|
* OTEL_LOG_LEVEL=info
|
|
59
59
|
*/
|
|
@@ -77,7 +77,7 @@ export function register() {
|
|
|
77
77
|
* SECURENOW_APPID=my-nextjs-app
|
|
78
78
|
*
|
|
79
79
|
* Optional:
|
|
80
|
-
* SECURENOW_INSTANCE=http://your-
|
|
80
|
+
* SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
81
81
|
* OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-key"
|
|
82
82
|
* OTEL_LOG_LEVEL=info
|
|
83
83
|
*
|
|
@@ -150,9 +150,9 @@ function createEnvTemplate(targetPath) {
|
|
|
150
150
|
# Required: Your application identifier
|
|
151
151
|
SECURENOW_APPID=my-nextjs-app
|
|
152
152
|
|
|
153
|
-
# Optional: Your
|
|
153
|
+
# Optional: Your SigNoz/OpenTelemetry collector endpoint
|
|
154
154
|
# Default: https://freetrial.securenow.ai:4318
|
|
155
|
-
SECURENOW_INSTANCE=http://your-
|
|
155
|
+
SECURENOW_INSTANCE=http://your-signoz-server:4318
|
|
156
156
|
|
|
157
157
|
# Optional: API key or authentication headers
|
|
158
158
|
# OTEL_EXPORTER_OTLP_HEADERS="x-api-key=your-api-key-here"
|
|
@@ -270,14 +270,14 @@ async function setup() {
|
|
|
270
270
|
console.log('│ │');
|
|
271
271
|
console.log('│ 1. Edit .env.local and set: │');
|
|
272
272
|
console.log('│ SECURENOW_APPID=your-app-name │');
|
|
273
|
-
console.log('│ SECURENOW_INSTANCE=http://
|
|
273
|
+
console.log('│ SECURENOW_INSTANCE=http://signoz:4318 │');
|
|
274
274
|
if (shouldCreateMiddleware) {
|
|
275
275
|
console.log('│ SECURENOW_CAPTURE_BODY=1 │');
|
|
276
276
|
}
|
|
277
277
|
console.log('│ │');
|
|
278
278
|
console.log('│ 2. Run your app: npm run dev │');
|
|
279
279
|
console.log('│ │');
|
|
280
|
-
console.log('│ 3. Check
|
|
280
|
+
console.log('│ 3. Check SigNoz for traces! │');
|
|
281
281
|
console.log('│ │');
|
|
282
282
|
if (shouldCreateMiddleware) {
|
|
283
283
|
console.log('│ 📝 Body capture enabled with auto-redaction │');
|
package/register.d.ts
CHANGED
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,15 +18,15 @@
|
|
|
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');
|
|
31
25
|
const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs');
|
|
26
|
+
const { logs: apiLogs } = require('@opentelemetry/api-logs');
|
|
32
27
|
const { Resource } = require('@opentelemetry/resources');
|
|
33
28
|
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
34
29
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
35
|
-
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
36
30
|
const { v4: uuidv4 } = require('uuid');
|
|
37
31
|
|
|
38
32
|
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
@@ -53,10 +47,6 @@ const DEFAULT_SENSITIVE_FIELDS = [
|
|
|
53
47
|
'card', 'cardnumber', 'ccv', 'cvc', 'cvv', 'ssn', 'pin',
|
|
54
48
|
];
|
|
55
49
|
|
|
56
|
-
function escapeRegex(str) {
|
|
57
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
50
|
/**
|
|
61
51
|
* Redact sensitive fields from an object
|
|
62
52
|
*/
|
|
@@ -65,7 +55,7 @@ function redactSensitiveData(obj, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
65
55
|
|
|
66
56
|
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
67
57
|
|
|
68
|
-
for (const key
|
|
58
|
+
for (const key in redacted) {
|
|
69
59
|
const lowerKey = key.toLowerCase();
|
|
70
60
|
|
|
71
61
|
if (sensitiveFields.some(field => lowerKey.includes(field.toLowerCase()))) {
|
|
@@ -88,10 +78,10 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
88
78
|
|
|
89
79
|
// Redact sensitive fields in GraphQL arguments and variables
|
|
90
80
|
sensitiveFields.forEach(field => {
|
|
91
|
-
|
|
81
|
+
// Match patterns: field: "value" or field: 'value' or field:"value"
|
|
92
82
|
const patterns = [
|
|
93
|
-
new RegExp(`(${
|
|
94
|
-
new RegExp(`(${
|
|
83
|
+
new RegExp(`(${field}\\s*:\\s*["'])([^"']+)(["'])`, 'gi'),
|
|
84
|
+
new RegExp(`(${field}\\s*:\\s*)([^\\s,})\n]+)`, 'gi'),
|
|
95
85
|
];
|
|
96
86
|
|
|
97
87
|
patterns.forEach(pattern => {
|
|
@@ -108,155 +98,6 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
108
98
|
return redacted;
|
|
109
99
|
}
|
|
110
100
|
|
|
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
101
|
// -------- diagnostics --------
|
|
261
102
|
(() => {
|
|
262
103
|
const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
@@ -320,44 +161,21 @@ for (const n of (env('SECURENOW_DISABLE_INSTRUMENTATIONS') || '').split(',').map
|
|
|
320
161
|
|
|
321
162
|
// -------- Body Capture Configuration --------
|
|
322
163
|
const captureBody = String(env('SECURENOW_CAPTURE_BODY')) === '1' || String(env('SECURENOW_CAPTURE_BODY')).toLowerCase() === 'true';
|
|
323
|
-
const maxBodySize =
|
|
164
|
+
const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB default
|
|
324
165
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
325
166
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
326
167
|
|
|
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
168
|
// Configure HTTP instrumentation with body capture
|
|
333
169
|
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
|
|
334
170
|
const httpInstrumentation = new HttpInstrumentation({
|
|
335
171
|
requestHook: (span, request) => {
|
|
336
172
|
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)) {
|
|
173
|
+
if (captureBody && request.method && ['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
356
174
|
const contentType = request.headers['content-type'] || '';
|
|
357
175
|
|
|
358
|
-
if (
|
|
176
|
+
if (contentType.includes('application/json') ||
|
|
359
177
|
contentType.includes('application/graphql') ||
|
|
360
|
-
contentType.includes('application/x-www-form-urlencoded'))
|
|
178
|
+
contentType.includes('application/x-www-form-urlencoded')) {
|
|
361
179
|
|
|
362
180
|
let body = '';
|
|
363
181
|
const chunks = [];
|
|
@@ -405,9 +223,9 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
405
223
|
});
|
|
406
224
|
}
|
|
407
225
|
} catch (e) {
|
|
408
|
-
|
|
226
|
+
// Parse error: capture as-is (truncated)
|
|
227
|
+
span.setAttribute('http.request.body', body.substring(0, 1000));
|
|
409
228
|
span.setAttribute('http.request.body.parse_error', true);
|
|
410
|
-
span.setAttribute('http.request.body.size', size);
|
|
411
229
|
}
|
|
412
230
|
} else if (size > maxBodySize) {
|
|
413
231
|
span.setAttribute('http.request.body', `[TOO LARGE: ${size} bytes]`);
|
|
@@ -415,38 +233,10 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
415
233
|
}
|
|
416
234
|
});
|
|
417
235
|
} 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
|
-
}
|
|
236
|
+
// Multipart is NOT captured
|
|
237
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
238
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
239
|
+
span.setAttribute('http.request.body.note', 'File uploads not captured by design');
|
|
450
240
|
}
|
|
451
241
|
}
|
|
452
242
|
} catch (error) {
|
|
@@ -456,7 +246,7 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
456
246
|
});
|
|
457
247
|
|
|
458
248
|
// -------- Logging Configuration --------
|
|
459
|
-
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED'))
|
|
249
|
+
const loggingEnabled = String(env('SECURENOW_LOGGING_ENABLED')) !== '0' && String(env('SECURENOW_LOGGING_ENABLED')).toLowerCase() !== 'false';
|
|
460
250
|
|
|
461
251
|
// Create shared resource for both traces and logs
|
|
462
252
|
const sharedResource = new Resource({
|
|
@@ -468,6 +258,7 @@ const sharedResource = new Resource({
|
|
|
468
258
|
|
|
469
259
|
// Initialize LoggerProvider if logging is enabled
|
|
470
260
|
let loggerProvider = null;
|
|
261
|
+
let globalLogger = null;
|
|
471
262
|
|
|
472
263
|
if (loggingEnabled) {
|
|
473
264
|
const logExporter = new OTLPLogExporter({
|
|
@@ -475,35 +266,16 @@ if (loggingEnabled) {
|
|
|
475
266
|
headers
|
|
476
267
|
});
|
|
477
268
|
|
|
478
|
-
const batchLogProcessor = new BatchLogRecordProcessor(logExporter);
|
|
479
269
|
loggerProvider = new LoggerProvider({
|
|
480
270
|
resource: sharedResource,
|
|
481
271
|
});
|
|
482
|
-
|
|
272
|
+
// sdk-logs 0.47.x ignores the `processors` constructor option (added in 0.52),
|
|
273
|
+
// so the provider would silently keep a NoopLogRecordProcessor and drop every
|
|
274
|
+
// emit(). Register the processor explicitly instead.
|
|
275
|
+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
|
|
276
|
+
apiLogs.setGlobalLoggerProvider(loggerProvider);
|
|
483
277
|
|
|
484
|
-
|
|
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;
|
|
278
|
+
globalLogger = loggerProvider.getLogger('securenow', '1.0.0');
|
|
507
279
|
}
|
|
508
280
|
|
|
509
281
|
// -------- SDK --------
|
|
@@ -512,11 +284,9 @@ const sdk = new NodeSDK({
|
|
|
512
284
|
traceExporter,
|
|
513
285
|
instrumentations: [
|
|
514
286
|
httpInstrumentation,
|
|
515
|
-
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
516
287
|
...getNodeAutoInstrumentations({
|
|
517
288
|
...disabledMap,
|
|
518
|
-
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
519
|
-
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
289
|
+
'@opentelemetry/instrumentation-http': { enabled: false }, // We use our custom one above
|
|
520
290
|
}),
|
|
521
291
|
],
|
|
522
292
|
resource: sharedResource,
|
|
@@ -535,52 +305,22 @@ const sdk = new NodeSDK({
|
|
|
535
305
|
if (captureBody) {
|
|
536
306
|
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
537
307
|
}
|
|
538
|
-
if (captureMultipart) {
|
|
539
|
-
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
540
|
-
}
|
|
541
308
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
542
309
|
const api = require('@opentelemetry/api');
|
|
543
310
|
const tracer = api.trace.getTracer('securenow-smoke');
|
|
544
311
|
const span = tracer.startSpan('securenow.startup.smoke'); span.end();
|
|
545
312
|
}
|
|
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
313
|
} catch (e) {
|
|
570
314
|
console.error('[securenow] OTel start failed:', e && e.stack || e);
|
|
571
315
|
}
|
|
572
316
|
})();
|
|
573
317
|
|
|
574
|
-
let shuttingDown = false;
|
|
575
318
|
async function safeShutdown(sig) {
|
|
576
|
-
if (shuttingDown) return;
|
|
577
|
-
shuttingDown = true;
|
|
578
319
|
try {
|
|
579
320
|
await Promise.resolve(sdk.shutdown?.());
|
|
580
321
|
if (loggerProvider) {
|
|
581
322
|
await Promise.resolve(loggerProvider.shutdown?.());
|
|
582
323
|
}
|
|
583
|
-
try { require('./firewall').shutdown(); } catch (_) {}
|
|
584
324
|
console.log(`[securenow] Tracing and logging terminated on ${sig}`);
|
|
585
325
|
}
|
|
586
326
|
catch (e) { console.error('[securenow] Shutdown error:', e); }
|