securenow 5.7.0 → 5.8.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/cli/security.js +151 -2
- package/cli.js +3 -2
- package/free-trial-banner.js +17 -3
- package/package.json +2 -1
- package/tracing.js +167 -5
- package/web-vite.mjs +17 -3
package/cli/security.js
CHANGED
|
@@ -269,20 +269,36 @@ async function trustedRemove(args, flags) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
// ── Helpers ──
|
|
273
|
+
|
|
274
|
+
async function resolveAppId(flags) {
|
|
275
|
+
const appKey = flags.app || config.getDefaultApp();
|
|
276
|
+
if (!appKey) return null;
|
|
277
|
+
const data = await api.get('/applications');
|
|
278
|
+
const apps = data.applications || [];
|
|
279
|
+
const match = apps.find(a => a.key === appKey);
|
|
280
|
+
return match ? { id: match._id, key: match.key, name: match.name } : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
272
283
|
// ── Forensics ──
|
|
273
284
|
|
|
274
285
|
async function forensicsQuery(args, flags) {
|
|
275
286
|
requireAuth();
|
|
276
287
|
const query = args.join(' ');
|
|
277
288
|
if (!query) {
|
|
278
|
-
ui.error('Query required. Usage: securenow forensics "your natural language query"');
|
|
289
|
+
ui.error('Query required. Usage: securenow forensics "your natural language query" [--app <key>]');
|
|
279
290
|
process.exit(1);
|
|
280
291
|
}
|
|
281
292
|
|
|
282
293
|
const s = ui.spinner('Submitting forensic query');
|
|
283
294
|
try {
|
|
284
295
|
const body = { query };
|
|
285
|
-
|
|
296
|
+
const resolved = await resolveAppId(flags);
|
|
297
|
+
if (resolved) {
|
|
298
|
+
body.applicationId = resolved.id;
|
|
299
|
+
} else if (flags.instance) {
|
|
300
|
+
body.instanceId = flags.instance;
|
|
301
|
+
}
|
|
286
302
|
|
|
287
303
|
const job = await api.post('/forensics/query', body);
|
|
288
304
|
const jobId = job.jobId;
|
|
@@ -387,6 +403,138 @@ async function forensicsLibrary(args, flags) {
|
|
|
387
403
|
}
|
|
388
404
|
}
|
|
389
405
|
|
|
406
|
+
// ── Forensics Chat ──
|
|
407
|
+
|
|
408
|
+
async function forensicsChat(args, flags) {
|
|
409
|
+
requireAuth();
|
|
410
|
+
const resolved = await resolveAppId(flags);
|
|
411
|
+
if (!resolved) {
|
|
412
|
+
ui.error('App required. Usage: securenow forensics chat --app <key>');
|
|
413
|
+
ui.info('Set a default with: securenow apps default <key>');
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log('');
|
|
418
|
+
ui.heading(`Forensics Chat — ${resolved.name || resolved.key}`);
|
|
419
|
+
console.log(ui.c.dim(` App: ${resolved.key} (${resolved.id})`));
|
|
420
|
+
console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
|
|
421
|
+
|
|
422
|
+
let conversationId = null;
|
|
423
|
+
|
|
424
|
+
const readline = require('readline');
|
|
425
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
426
|
+
|
|
427
|
+
const askLine = () => new Promise((resolve) => {
|
|
428
|
+
rl.question(ui.c.cyan(' you > '), (answer) => resolve(answer.trim()));
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
while (true) {
|
|
433
|
+
const message = await askLine();
|
|
434
|
+
if (!message || message.toLowerCase() === 'exit' || message.toLowerCase() === 'quit') {
|
|
435
|
+
console.log('');
|
|
436
|
+
ui.info('Chat ended.');
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const s = ui.spinner('Thinking');
|
|
441
|
+
try {
|
|
442
|
+
const body = { message, applicationKey: resolved.key };
|
|
443
|
+
if (conversationId) body.conversationId = conversationId;
|
|
444
|
+
|
|
445
|
+
const chatRes = await api.post('/forensics/chat', body);
|
|
446
|
+
conversationId = chatRes.conversationId;
|
|
447
|
+
|
|
448
|
+
if (chatRes.status === 'processing') {
|
|
449
|
+
let result;
|
|
450
|
+
for (let i = 0; i < 150; i++) {
|
|
451
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
452
|
+
result = await api.get(`/forensics/chat/status/${conversationId}`);
|
|
453
|
+
if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
|
|
454
|
+
const progress = result._progress;
|
|
455
|
+
if (progress?.action) s.update(progress.action);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (!result || result.status === 'processing') {
|
|
459
|
+
s.fail('Response timed out');
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
s.stop('Done');
|
|
464
|
+
printChatMessage(result.message);
|
|
465
|
+
|
|
466
|
+
if (result.status === 'awaiting_confirmation') {
|
|
467
|
+
const proceed = await ui.confirm('Proceed with this query?');
|
|
468
|
+
if (proceed) {
|
|
469
|
+
const cs = ui.spinner('Executing query');
|
|
470
|
+
await api.post(`/forensics/chat/confirm/${conversationId}`);
|
|
471
|
+
let confirmResult;
|
|
472
|
+
for (let i = 0; i < 90; i++) {
|
|
473
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
474
|
+
confirmResult = await api.get(`/forensics/chat/status/${conversationId}`);
|
|
475
|
+
if (confirmResult.status !== 'processing') break;
|
|
476
|
+
}
|
|
477
|
+
cs.stop('Done');
|
|
478
|
+
if (confirmResult?.message) printChatMessage(confirmResult.message);
|
|
479
|
+
} else {
|
|
480
|
+
ui.info('Query skipped. Ask a different question or refine.');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} else if (chatRes.message) {
|
|
484
|
+
s.stop('Done');
|
|
485
|
+
printChatMessage(chatRes.message);
|
|
486
|
+
} else {
|
|
487
|
+
s.stop('Done');
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
s.fail('Error');
|
|
491
|
+
ui.error(err.message);
|
|
492
|
+
}
|
|
493
|
+
console.log('');
|
|
494
|
+
}
|
|
495
|
+
} finally {
|
|
496
|
+
rl.close();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function printChatMessage(msg) {
|
|
501
|
+
if (!msg) return;
|
|
502
|
+
console.log('');
|
|
503
|
+
if (msg.content) {
|
|
504
|
+
console.log(` ${ui.c.green('ai >')} ${msg.content}`);
|
|
505
|
+
}
|
|
506
|
+
if (msg.sqlQuery) {
|
|
507
|
+
console.log('');
|
|
508
|
+
ui.subheading('Generated SQL');
|
|
509
|
+
console.log(`\n ${ui.c.dim(msg.sqlQuery)}\n`);
|
|
510
|
+
}
|
|
511
|
+
if (msg.results && Array.isArray(msg.results) && msg.results.length > 0) {
|
|
512
|
+
ui.subheading(`Results (${msg.resultCount ?? msg.results.length} rows)`);
|
|
513
|
+
console.log('');
|
|
514
|
+
const headers = Object.keys(msg.results[0]);
|
|
515
|
+
const rows = msg.results.map(row => headers.map(h => String(row[h] ?? '')));
|
|
516
|
+
ui.table(headers, rows);
|
|
517
|
+
}
|
|
518
|
+
if (msg.estimation) {
|
|
519
|
+
const est = msg.estimation;
|
|
520
|
+
const rowLabel = est.timedOut
|
|
521
|
+
? 'very large (estimation timed out)'
|
|
522
|
+
: est.estimatedRows != null
|
|
523
|
+
? `~${Number(est.estimatedRows).toLocaleString()}`
|
|
524
|
+
: 'unknown';
|
|
525
|
+
console.log(`\n ${ui.c.yellow('!')} Estimated rows: ${rowLabel}`);
|
|
526
|
+
if (est.suggestions?.length) {
|
|
527
|
+
console.log(ui.c.dim(' Suggestions:'));
|
|
528
|
+
for (const sug of est.suggestions) {
|
|
529
|
+
console.log(ui.c.dim(` • ${sug}`));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (msg.error && !msg.results) {
|
|
534
|
+
console.log(`\n ${ui.c.red('Error:')} ${msg.error}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
390
538
|
// ── IP Lookup ──
|
|
391
539
|
|
|
392
540
|
async function ipLookup(args, flags) {
|
|
@@ -670,6 +818,7 @@ module.exports = {
|
|
|
670
818
|
trustedAdd,
|
|
671
819
|
trustedRemove,
|
|
672
820
|
forensicsQuery,
|
|
821
|
+
forensicsChat,
|
|
673
822
|
forensicsLibrary,
|
|
674
823
|
ipLookup,
|
|
675
824
|
ipTraces,
|
package/cli.js
CHANGED
|
@@ -151,9 +151,10 @@ const COMMANDS = {
|
|
|
151
151
|
},
|
|
152
152
|
forensics: {
|
|
153
153
|
desc: 'Run forensic queries (natural language → SQL)',
|
|
154
|
-
usage: 'securenow forensics <query>',
|
|
154
|
+
usage: 'securenow forensics <query> [--app <key>]',
|
|
155
155
|
sub: {
|
|
156
|
-
query: { desc: 'Run a forensic query', run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
|
|
156
|
+
query: { desc: 'Run a forensic query', flags: { app: 'App key to scope query' }, run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
|
|
157
|
+
chat: { desc: 'Interactive forensics chat (scoped to an app)', usage: 'securenow forensics chat --app <key>', flags: { app: 'App key to chat with' }, run: (a, f) => require('./cli/security').forensicsChat(a, f) },
|
|
157
158
|
library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
|
|
158
159
|
},
|
|
159
160
|
defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
|
package/free-trial-banner.js
CHANGED
|
@@ -40,19 +40,33 @@ function _bannerClientCode() {
|
|
|
40
40
|
strong.textContent = 'Testing Environment:';
|
|
41
41
|
msg.appendChild(strong);
|
|
42
42
|
msg.appendChild(document.createTextNode(
|
|
43
|
-
' Only add test applications. For production usage,
|
|
43
|
+
' Only add test applications. For production usage, '
|
|
44
44
|
));
|
|
45
45
|
|
|
46
46
|
var link = document.createElement('a');
|
|
47
|
-
link.href = 'https://securenow.ai/
|
|
47
|
+
link.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
48
48
|
link.target = '_blank';
|
|
49
49
|
link.rel = 'noopener';
|
|
50
50
|
link.style.cssText = 'color:#664D03;font-weight:600;text-decoration:underline';
|
|
51
|
-
link.textContent = '
|
|
51
|
+
link.textContent = 'create a new production instance';
|
|
52
52
|
msg.appendChild(link);
|
|
53
53
|
msg.appendChild(document.createTextNode('.'));
|
|
54
54
|
d.appendChild(msg);
|
|
55
55
|
|
|
56
|
+
var upgradeBtn = document.createElement('a');
|
|
57
|
+
upgradeBtn.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
58
|
+
upgradeBtn.target = '_blank';
|
|
59
|
+
upgradeBtn.rel = 'noopener';
|
|
60
|
+
upgradeBtn.textContent = '\u26a1 Upgrade';
|
|
61
|
+
upgradeBtn.style.cssText =
|
|
62
|
+
'display:inline-flex;align-items:center;gap:4px;' +
|
|
63
|
+
'background:#D97706;color:#fff;padding:4px 12px;border-radius:4px;' +
|
|
64
|
+
'font-size:12px;font-weight:600;text-decoration:none;margin-left:10px;' +
|
|
65
|
+
'white-space:nowrap;transition:background 0.15s';
|
|
66
|
+
upgradeBtn.onmouseover = function () { upgradeBtn.style.background = '#B45309'; };
|
|
67
|
+
upgradeBtn.onmouseout = function () { upgradeBtn.style.background = '#D97706'; };
|
|
68
|
+
d.appendChild(upgradeBtn);
|
|
69
|
+
|
|
56
70
|
var close = document.createElement('button');
|
|
57
71
|
close.textContent = '\u00d7';
|
|
58
72
|
close.style.cssText =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
"@opentelemetry/instrumentation-document-load": "0.47.0",
|
|
121
121
|
"@opentelemetry/instrumentation-fetch": "0.47.0",
|
|
122
122
|
"@opentelemetry/instrumentation-http": "0.47.0",
|
|
123
|
+
"@opentelemetry/instrumentation-mongodb": "0.46.0",
|
|
123
124
|
"@opentelemetry/instrumentation-user-interaction": "0.47.0",
|
|
124
125
|
"@opentelemetry/instrumentation-xml-http-request": "0.47.0",
|
|
125
126
|
"@opentelemetry/resources": "1.20.0",
|
package/tracing.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=... # full traces URL
|
|
17
17
|
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
18
18
|
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
19
|
+
* SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
|
|
19
20
|
* OTEL_LOG_LEVEL=info|debug
|
|
20
21
|
* SECURENOW_TEST_SPAN=1
|
|
21
22
|
*
|
|
@@ -31,6 +32,7 @@ const { LoggerProvider, BatchLogRecordProcessor } = require('@opentelemetry/sdk-
|
|
|
31
32
|
const { Resource } = require('@opentelemetry/resources');
|
|
32
33
|
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
|
|
33
34
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
|
|
35
|
+
const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb');
|
|
34
36
|
const { v4: uuidv4 } = require('uuid');
|
|
35
37
|
|
|
36
38
|
const env = k => process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
@@ -102,6 +104,131 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
|
|
|
102
104
|
return redacted;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
// -------- Multipart streaming parser --------
|
|
108
|
+
// Streams through the request without buffering file content.
|
|
109
|
+
// Only part headers and text-field values are kept in memory,
|
|
110
|
+
// so memory stays bounded (~few KB) regardless of upload size.
|
|
111
|
+
|
|
112
|
+
function extractBoundary(contentType) {
|
|
113
|
+
const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/i);
|
|
114
|
+
return match ? (match[1] || match[2]) : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFieldSize, onComplete) {
|
|
118
|
+
const boundary = extractBoundary(contentType);
|
|
119
|
+
if (!boundary) { onComplete({ error: 'BOUNDARY_NOT_FOUND' }); return; }
|
|
120
|
+
|
|
121
|
+
const result = { fields: {}, files: [] };
|
|
122
|
+
let totalSize = 0;
|
|
123
|
+
let buf = Buffer.alloc(0);
|
|
124
|
+
|
|
125
|
+
const MAX_PARTS = 100;
|
|
126
|
+
let partCount = 0;
|
|
127
|
+
|
|
128
|
+
const FIRST_DELIM = Buffer.from('--' + boundary);
|
|
129
|
+
const DELIM = Buffer.from('\r\n--' + boundary);
|
|
130
|
+
const HDR_END = Buffer.from('\r\n\r\n');
|
|
131
|
+
|
|
132
|
+
let initialized = false;
|
|
133
|
+
let inHeaders = true;
|
|
134
|
+
let isFile = false;
|
|
135
|
+
let fldName = '';
|
|
136
|
+
let fName = '';
|
|
137
|
+
let pCT = '';
|
|
138
|
+
let bodyBytes = 0;
|
|
139
|
+
let textVal = '';
|
|
140
|
+
|
|
141
|
+
function flushPart() {
|
|
142
|
+
if (!fldName) return;
|
|
143
|
+
if (isFile) {
|
|
144
|
+
result.files.push({ field: fldName, filename: fName, contentType: pCT || 'unknown', size: bodyBytes });
|
|
145
|
+
} else {
|
|
146
|
+
const lower = fldName.toLowerCase();
|
|
147
|
+
const redact = sensitiveFields.some(f => lower.includes(f.toLowerCase()));
|
|
148
|
+
result.fields[fldName] = redact ? '[REDACTED]' : textVal.substring(0, maxTextFieldSize);
|
|
149
|
+
}
|
|
150
|
+
fldName = ''; bodyBytes = 0; textVal = ''; partCount++;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function drain() {
|
|
154
|
+
if (!initialized) {
|
|
155
|
+
const i = buf.indexOf(FIRST_DELIM);
|
|
156
|
+
if (i === -1) {
|
|
157
|
+
if (buf.length > FIRST_DELIM.length + 4) buf = buf.slice(buf.length - FIRST_DELIM.length - 4);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
buf = buf.slice(i + FIRST_DELIM.length);
|
|
161
|
+
initialized = true;
|
|
162
|
+
inHeaders = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let guard = 200;
|
|
166
|
+
while (buf.length > 0 && guard-- > 0 && partCount < MAX_PARTS) {
|
|
167
|
+
if (inHeaders) {
|
|
168
|
+
if (buf.length >= 2 && buf[0] === 0x2D && buf[1] === 0x2D) { buf = Buffer.alloc(0); return; }
|
|
169
|
+
if (buf.length >= 2 && buf[0] === 0x0D && buf[1] === 0x0A) { buf = buf.slice(2); continue; }
|
|
170
|
+
|
|
171
|
+
const hi = buf.indexOf(HDR_END);
|
|
172
|
+
if (hi === -1) return;
|
|
173
|
+
|
|
174
|
+
const hdr = buf.slice(0, hi).toString('latin1');
|
|
175
|
+
buf = buf.slice(hi + 4);
|
|
176
|
+
|
|
177
|
+
const nm = hdr.match(/name="([^"]+)"/);
|
|
178
|
+
const fn = hdr.match(/filename="([^"]*)"/);
|
|
179
|
+
const ct = hdr.match(/Content-Type:\s*(.+)/i);
|
|
180
|
+
fldName = nm ? nm[1] : '';
|
|
181
|
+
fName = fn ? fn[1] : '';
|
|
182
|
+
pCT = ct ? ct[1].trim() : '';
|
|
183
|
+
isFile = !!fn;
|
|
184
|
+
bodyBytes = 0;
|
|
185
|
+
textVal = '';
|
|
186
|
+
inHeaders = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const di = buf.indexOf(DELIM);
|
|
190
|
+
if (di === -1) {
|
|
191
|
+
const safe = Math.max(0, buf.length - DELIM.length - 2);
|
|
192
|
+
if (safe > 0) {
|
|
193
|
+
bodyBytes += safe;
|
|
194
|
+
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
195
|
+
textVal += buf.slice(0, safe).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
196
|
+
}
|
|
197
|
+
buf = buf.slice(safe);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
bodyBytes += di;
|
|
203
|
+
if (!isFile && textVal.length < maxTextFieldSize) {
|
|
204
|
+
textVal += buf.slice(0, di).toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
205
|
+
}
|
|
206
|
+
flushPart();
|
|
207
|
+
buf = buf.slice(di + DELIM.length);
|
|
208
|
+
inHeaders = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
request.on('data', (chunk) => {
|
|
213
|
+
totalSize += chunk.length;
|
|
214
|
+
buf = Buffer.concat([buf, chunk]);
|
|
215
|
+
drain();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
request.on('end', () => {
|
|
219
|
+
try {
|
|
220
|
+
if (!inHeaders && fldName) {
|
|
221
|
+
bodyBytes += buf.length;
|
|
222
|
+
if (!isFile) textVal += buf.toString('utf8').substring(0, maxTextFieldSize - textVal.length);
|
|
223
|
+
flushPart();
|
|
224
|
+
}
|
|
225
|
+
onComplete({ parsed: result, totalSize });
|
|
226
|
+
} catch (e) {
|
|
227
|
+
onComplete({ error: 'PARSE_ERROR' });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
105
232
|
// -------- ESM detection --------
|
|
106
233
|
// register.js auto-registers the hook via module.register() on Node >=20.6.
|
|
107
234
|
// This warning only fires if BOTH --import AND module.register() were skipped
|
|
@@ -193,6 +320,8 @@ const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB
|
|
|
193
320
|
const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
194
321
|
const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
|
|
195
322
|
|
|
323
|
+
const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || String(env('SECURENOW_CAPTURE_MULTIPART')).toLowerCase() === 'true';
|
|
324
|
+
|
|
196
325
|
// -------- Trusted proxy IP resolution --------
|
|
197
326
|
// Only trust X-Forwarded-For / X-Real-IP when the direct connection comes from
|
|
198
327
|
// a known proxy (loopback, private RFC-1918/RFC-4193, or an explicit allowlist).
|
|
@@ -324,10 +453,38 @@ const httpInstrumentation = new HttpInstrumentation({
|
|
|
324
453
|
}
|
|
325
454
|
});
|
|
326
455
|
} else if (contentType.includes('multipart/form-data')) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
456
|
+
if (captureMultipart) {
|
|
457
|
+
collectMultipartMeta(request, contentType, allSensitiveFields, 1000, ({ error, parsed, totalSize }) => {
|
|
458
|
+
try {
|
|
459
|
+
if (error === 'BOUNDARY_NOT_FOUND') {
|
|
460
|
+
span.setAttribute('http.request.body', '[MULTIPART - BOUNDARY NOT FOUND]');
|
|
461
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (error) {
|
|
465
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
466
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
467
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
span.setAttributes({
|
|
471
|
+
'http.request.body': JSON.stringify(parsed).substring(0, maxBodySize),
|
|
472
|
+
'http.request.body.type': 'multipart',
|
|
473
|
+
'http.request.body.size': totalSize,
|
|
474
|
+
'http.request.body.fields_count': Object.keys(parsed.fields).length,
|
|
475
|
+
'http.request.body.files_count': parsed.files.length,
|
|
476
|
+
});
|
|
477
|
+
} catch (e) {
|
|
478
|
+
span.setAttribute('http.request.body', '[MULTIPART - PARSE ERROR]');
|
|
479
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
480
|
+
span.setAttribute('http.request.body.parse_error', true);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
} else {
|
|
484
|
+
span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
|
|
485
|
+
span.setAttribute('http.request.body.type', 'multipart');
|
|
486
|
+
span.setAttribute('http.request.body.note', 'Set SECURENOW_CAPTURE_MULTIPART=1 to enable');
|
|
487
|
+
}
|
|
331
488
|
}
|
|
332
489
|
}
|
|
333
490
|
} catch (error) {
|
|
@@ -392,9 +549,11 @@ const sdk = new NodeSDK({
|
|
|
392
549
|
traceExporter,
|
|
393
550
|
instrumentations: [
|
|
394
551
|
httpInstrumentation,
|
|
552
|
+
...(disabledMap['@opentelemetry/instrumentation-mongodb'] ? [] : [new MongoDBInstrumentation()]),
|
|
395
553
|
...getNodeAutoInstrumentations({
|
|
396
554
|
...disabledMap,
|
|
397
|
-
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
555
|
+
'@opentelemetry/instrumentation-http': { enabled: false },
|
|
556
|
+
'@opentelemetry/instrumentation-mongodb': { enabled: false },
|
|
398
557
|
}),
|
|
399
558
|
],
|
|
400
559
|
resource: sharedResource,
|
|
@@ -413,6 +572,9 @@ const sdk = new NodeSDK({
|
|
|
413
572
|
if (captureBody) {
|
|
414
573
|
console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
|
|
415
574
|
}
|
|
575
|
+
if (captureMultipart) {
|
|
576
|
+
console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
|
|
577
|
+
}
|
|
416
578
|
if (String(env('SECURENOW_TEST_SPAN')) === '1') {
|
|
417
579
|
const api = require('@opentelemetry/api');
|
|
418
580
|
const tracer = api.trace.getTracer('securenow-smoke');
|
package/web-vite.mjs
CHANGED
|
@@ -173,19 +173,33 @@ function injectFreeTrialBanner(): void {
|
|
|
173
173
|
strong.textContent = 'Testing Environment:';
|
|
174
174
|
msg.appendChild(strong);
|
|
175
175
|
msg.appendChild(document.createTextNode(
|
|
176
|
-
' Only add test applications. For production usage,
|
|
176
|
+
' Only add test applications. For production usage, '
|
|
177
177
|
));
|
|
178
178
|
|
|
179
179
|
const link = document.createElement('a');
|
|
180
|
-
link.href = 'https://securenow.ai/
|
|
180
|
+
link.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
181
181
|
link.target = '_blank';
|
|
182
182
|
link.rel = 'noopener';
|
|
183
183
|
link.style.cssText = 'color:#664D03;font-weight:600;text-decoration:underline';
|
|
184
|
-
link.textContent = '
|
|
184
|
+
link.textContent = 'create a new production instance';
|
|
185
185
|
msg.appendChild(link);
|
|
186
186
|
msg.appendChild(document.createTextNode('.'));
|
|
187
187
|
d.appendChild(msg);
|
|
188
188
|
|
|
189
|
+
const upgradeBtn = document.createElement('a');
|
|
190
|
+
upgradeBtn.href = 'https://app.securenow.ai/dashboard/settings/instances';
|
|
191
|
+
upgradeBtn.target = '_blank';
|
|
192
|
+
upgradeBtn.rel = 'noopener';
|
|
193
|
+
upgradeBtn.textContent = '\u26a1 Upgrade';
|
|
194
|
+
upgradeBtn.style.cssText =
|
|
195
|
+
'display:inline-flex;align-items:center;gap:4px;' +
|
|
196
|
+
'background:#D97706;color:#fff;padding:4px 12px;border-radius:4px;' +
|
|
197
|
+
'font-size:12px;font-weight:600;text-decoration:none;margin-left:10px;' +
|
|
198
|
+
'white-space:nowrap;transition:background 0.15s';
|
|
199
|
+
upgradeBtn.onmouseover = () => { upgradeBtn.style.background = '#B45309'; };
|
|
200
|
+
upgradeBtn.onmouseout = () => { upgradeBtn.style.background = '#D97706'; };
|
|
201
|
+
d.appendChild(upgradeBtn);
|
|
202
|
+
|
|
189
203
|
const close = document.createElement('button');
|
|
190
204
|
close.textContent = '\u00d7';
|
|
191
205
|
close.style.cssText =
|