securenow 5.7.1 → 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 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
- if (flags.instance) body.instanceId = flags.instance;
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
4
  const ui = require('./cli/ui');
@@ -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),
@@ -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, please '
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/contact';
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 = 'contact our team for a demo';
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.7.1",
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",
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
  *
@@ -103,6 +104,131 @@ function redactGraphQLQuery(query, sensitiveFields = DEFAULT_SENSITIVE_FIELDS) {
103
104
  return redacted;
104
105
  }
105
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
+
106
232
  // -------- ESM detection --------
107
233
  // register.js auto-registers the hook via module.register() on Node >=20.6.
108
234
  // This warning only fires if BOTH --import AND module.register() were skipped
@@ -194,6 +320,8 @@ const maxBodySize = parseInt(env('SECURENOW_MAX_BODY_SIZE') || '10240'); // 10KB
194
320
  const customSensitiveFields = (env('SECURENOW_SENSITIVE_FIELDS') || '').split(',').map(s => s.trim()).filter(Boolean);
195
321
  const allSensitiveFields = [...DEFAULT_SENSITIVE_FIELDS, ...customSensitiveFields];
196
322
 
323
+ const captureMultipart = String(env('SECURENOW_CAPTURE_MULTIPART')) === '1' || String(env('SECURENOW_CAPTURE_MULTIPART')).toLowerCase() === 'true';
324
+
197
325
  // -------- Trusted proxy IP resolution --------
198
326
  // Only trust X-Forwarded-For / X-Real-IP when the direct connection comes from
199
327
  // a known proxy (loopback, private RFC-1918/RFC-4193, or an explicit allowlist).
@@ -325,10 +453,38 @@ const httpInstrumentation = new HttpInstrumentation({
325
453
  }
326
454
  });
327
455
  } else if (contentType.includes('multipart/form-data')) {
328
- // Multipart is NOT captured
329
- span.setAttribute('http.request.body', '[MULTIPART - NOT CAPTURED]');
330
- span.setAttribute('http.request.body.type', 'multipart');
331
- span.setAttribute('http.request.body.note', 'File uploads not captured by design');
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
+ }
332
488
  }
333
489
  }
334
490
  } catch (error) {
@@ -416,6 +572,9 @@ const sdk = new NodeSDK({
416
572
  if (captureBody) {
417
573
  console.log('[securenow] 📝 Request body capture: ENABLED (max: %d bytes, redacting %d sensitive fields)', maxBodySize, allSensitiveFields.length);
418
574
  }
575
+ if (captureMultipart) {
576
+ console.log('[securenow] 📎 Multipart body capture: ENABLED (streaming — file content not buffered)');
577
+ }
419
578
  if (String(env('SECURENOW_TEST_SPAN')) === '1') {
420
579
  const api = require('@opentelemetry/api');
421
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, please '
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/contact';
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 = 'contact our team for a demo';
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 =