securenow 5.17.1 → 5.19.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/NPM_README.md CHANGED
@@ -211,20 +211,6 @@ npx securenow logs --app <key> --minutes 30 --level error
211
211
  npx securenow logs trace <traceId>
212
212
  ```
213
213
 
214
- ### Security Issues
215
-
216
- ```bash
217
- # List all issues
218
- npx securenow issues
219
- npx securenow issues --status open
220
-
221
- # Show issue details with AI analysis
222
- npx securenow issues show <issue-id>
223
-
224
- # Resolve an issue
225
- npx securenow issues resolve <issue-id>
226
- ```
227
-
228
214
  ### Notifications
229
215
 
230
216
  ```bash
@@ -382,21 +368,13 @@ You can also use the `SECURENOW_TOKEN` env var for per-terminal sessions without
382
368
 
383
369
  ```bash
384
370
  # Authenticate with a token in CI (env var — no file needed)
385
- SECURENOW_TOKEN=$MY_SECRET npx securenow issues --json
371
+ SECURENOW_TOKEN=$MY_SECRET npx securenow logs --json
386
372
 
387
373
  # Or use login with explicit token
388
374
  npx securenow login --token $SECURENOW_TOKEN
389
375
 
390
376
  # Use --json for machine-readable output
391
- npx securenow issues --json | jq '.[] | select(.severity == "critical")'
392
-
393
- # Check for critical issues in a pipeline
394
- ISSUES=$(npx securenow issues --json --status open)
395
- CRITICAL=$(echo "$ISSUES" | jq '[.[] | select(.severity == "critical")] | length')
396
- if [ "$CRITICAL" -gt "0" ]; then
397
- echo "Found $CRITICAL critical issues!"
398
- exit 1
399
- fi
377
+ npx securenow logs --json --level error | jq '.logs'
400
378
  ```
401
379
 
402
380
  ### Complete Command Reference
@@ -419,10 +397,7 @@ fi
419
397
  | | `logs trace <id>` | Logs for a trace |
420
398
  | | `analytics` | Response analytics |
421
399
  | | `status` | Dashboard overview |
422
- | **Detect** | `issues` | List issues |
423
- | | `issues show <id>` | Issue details |
424
- | | `issues resolve <id>` | Resolve issue |
425
- | | `notifications` | List notifications |
400
+ | **Detect** | `notifications` | List notifications |
426
401
  | | `notifications unread` | Unread count |
427
402
  | | `notifications read <id>` | Mark read |
428
403
  | | `notifications read-all` | Mark all read |
package/README.md CHANGED
@@ -117,10 +117,9 @@ npx securenow apps create my-app
117
117
  # Set it as default so you don't need --app every time
118
118
  npx securenow config set defaultApp <key>
119
119
 
120
- # View traces, logs, issues
120
+ # View traces, logs
121
121
  npx securenow traces
122
122
  npx securenow logs
123
- npx securenow issues
124
123
 
125
124
  # IP intelligence, forensic queries, blocklist
126
125
  npx securenow ip 1.2.3.4
@@ -295,9 +294,6 @@ Most users won't need this — just add `-r securenow/register` to your existing
295
294
 
296
295
  | Command | Description |
297
296
  |---------|-------------|
298
- | `securenow issues` | List security issues |
299
- | `securenow issues show <id>` | Show issue details and AI analysis |
300
- | `securenow issues resolve <id>` | Mark an issue as resolved |
301
297
  | `securenow notifications` | List notifications |
302
298
  | `securenow notifications unread` | Show unread count |
303
299
  | `securenow notifications read <id>` | Mark notification as read |
package/SKILL-CLI.md CHANGED
@@ -159,15 +159,6 @@ securenow logs list --app my-app --minutes 30 --level error
159
159
  securenow logs trace <traceId> # logs correlated to a specific trace
160
160
  ```
161
161
 
162
- ### Issues
163
-
164
- ```bash
165
- securenow issues [--app <key>] [--status open|resolved]
166
- securenow issues list --status open
167
- securenow issues show <id> # full issue details
168
- securenow issues resolve <id> # mark as resolved
169
- ```
170
-
171
162
  ### Notifications
172
163
 
173
164
  ```bash
@@ -342,7 +333,7 @@ securenow ip traces <attacker-ip> --json
342
333
  securenow traces show <trace-id> --json
343
334
  securenow traces analyze <trace-id> --json
344
335
  # Decision: block the IP
345
- securenow blocklist add <attacker-ip> --reason "Automated: SQL injection from issue #<id>"
336
+ securenow blocklist add <attacker-ip> --reason "Automated: SQL injection detected"
346
337
  ```
347
338
 
348
339
  #### Notification IP Investigation Statuses
@@ -387,7 +378,6 @@ securenow status --json
387
378
  securenow firewall status --json
388
379
  securenow blocklist stats --json
389
380
  securenow api-map stats --json
390
- securenow issues list --status open --json
391
381
  securenow forensics "summarize all attacks in the last 7 days"
392
382
  ```
393
383
 
package/cli/monitor.js CHANGED
@@ -292,108 +292,6 @@ async function logsTrace(args, flags) {
292
292
  }
293
293
  }
294
294
 
295
- // ── Issues ──
296
-
297
- async function issuesList(args, flags) {
298
- requireAuth();
299
- const s = ui.spinner('Fetching issues');
300
- try {
301
- const query = {};
302
- const appKey = resolveApp(flags);
303
- if (appKey) query.serviceName = appKey;
304
- if (flags.status) query.status = flags.status;
305
-
306
- const data = await api.get('/issues', { query });
307
- const issues = data.issues || [];
308
- s.stop(`Found ${issues.length} issue${issues.length !== 1 ? 's' : ''}`);
309
-
310
- if (flags.json) { ui.json(data); return; }
311
-
312
- console.log('');
313
- const rows = issues.map(i => [
314
- ui.c.dim(ui.truncate(i._id, 12)),
315
- ui.statusBadge(i.severity || i.level || 'medium'),
316
- ui.statusBadge(i.status || 'open'),
317
- ui.truncate(i.title || i.message || i.type || '', 50),
318
- i.serviceName || '—',
319
- i.count != null ? String(i.count) : '—',
320
- ui.timeAgo(i.lastSeen || i.createdAt),
321
- ]);
322
-
323
- ui.table(['ID', 'Severity', 'Status', 'Title', 'App', 'Count', 'Last Seen'], rows);
324
- if (data.total != null) {
325
- console.log(ui.c.dim(` Total: ${data.total}`));
326
- }
327
- console.log('');
328
- } catch (err) {
329
- s.fail('Failed to fetch issues');
330
- throw err;
331
- }
332
- }
333
-
334
- async function issuesShow(args, flags) {
335
- requireAuth();
336
- const id = args[0];
337
- if (!id) {
338
- ui.error('Issue ID required. Usage: securenow issues show <id>');
339
- process.exit(1);
340
- }
341
-
342
- const s = ui.spinner('Fetching issue');
343
- try {
344
- const data = await api.get(`/issues/${id}`);
345
- const issue = data.issue || data;
346
- s.stop('Issue loaded');
347
-
348
- if (flags.json) { ui.json(issue); return; }
349
-
350
- console.log('');
351
- ui.heading(issue.title || issue.type || `Issue ${id}`);
352
- console.log('');
353
- ui.keyValue([
354
- ['ID', issue._id],
355
- ['Status', ui.statusBadge(issue.status || 'open')],
356
- ['Severity', ui.statusBadge(issue.severity || issue.level || 'medium')],
357
- ['App', issue.serviceName || '—'],
358
- ['Type', issue.type || '—'],
359
- ['Count', issue.count != null ? String(issue.count) : '—'],
360
- ['First Seen', issue.firstSeen ? new Date(issue.firstSeen).toLocaleString() : '—'],
361
- ['Last Seen', issue.lastSeen ? new Date(issue.lastSeen).toLocaleString() : '—'],
362
- ]);
363
-
364
- if (issue.message || issue.description) {
365
- ui.subheading('Description');
366
- console.log(`\n ${issue.message || issue.description}\n`);
367
- }
368
-
369
- if (issue.analysis) {
370
- ui.subheading('AI Analysis');
371
- console.log(`\n ${issue.analysis}\n`);
372
- }
373
- console.log('');
374
- } catch (err) {
375
- s.fail('Failed to fetch issue');
376
- throw err;
377
- }
378
- }
379
-
380
- async function issuesResolve(args, flags) {
381
- requireAuth();
382
- const id = args[0];
383
- if (!id) {
384
- ui.error('Issue ID required. Usage: securenow issues resolve <id>');
385
- process.exit(1);
386
- }
387
-
388
- const s = ui.spinner('Resolving issue');
389
- try {
390
- await api.patch(`/issues/${id}`, { status: 'resolved' });
391
- s.stop('Issue resolved');
392
- } catch (err) {
393
- s.fail('Failed to resolve issue');
394
- throw err;
395
- }
396
- }
397
295
 
398
296
  // ── Notifications ──
399
297
 
@@ -534,9 +432,6 @@ module.exports = {
534
432
  tracesAnalyze,
535
433
  logsList,
536
434
  logsTrace,
537
- issuesList,
538
- issuesShow,
539
- issuesResolve,
540
435
  notificationsList,
541
436
  notificationsRead,
542
437
  notificationsReadAll,
package/cli/security.js CHANGED
@@ -861,88 +861,6 @@ async function ipTraces(args, flags) {
861
861
  }
862
862
  }
863
863
 
864
- // ── API Map ──
865
-
866
- async function apiMapList(args, flags) {
867
- requireAuth();
868
- const s = ui.spinner('Fetching API map');
869
- try {
870
- const data = await api.get('/api-map');
871
- const apiMap = data.apiMap;
872
- s.stop('API map loaded');
873
-
874
- if (flags.json) { ui.json(data); return; }
875
-
876
- if (!apiMap) {
877
- console.log('');
878
- ui.info(data.message || 'No API map discovered yet. Run discovery from the dashboard.');
879
- console.log('');
880
- return;
881
- }
882
-
883
- console.log('');
884
- if (apiMap.apps && typeof apiMap.apps === 'object') {
885
- for (const [appName, appData] of Object.entries(apiMap.apps)) {
886
- ui.subheading(appName);
887
- console.log('');
888
- const endpoints = appData.endpoints || [];
889
- if (endpoints.length) {
890
- const rows = endpoints.map(e => [
891
- e.method || '—',
892
- e.path || e.route || '—',
893
- e.requestCount != null ? String(e.requestCount) : '—',
894
- e.description || ui.c.dim('—'),
895
- ]);
896
- ui.table(['Method', 'Path', 'Requests', 'Description'], rows);
897
- } else {
898
- ui.info('No endpoints discovered for this app.');
899
- }
900
- console.log('');
901
- }
902
- } else {
903
- ui.json(apiMap);
904
- }
905
- } catch (err) {
906
- s.fail('Failed to fetch API map');
907
- throw err;
908
- }
909
- }
910
-
911
- async function apiMapStats(args, flags) {
912
- requireAuth();
913
- const s = ui.spinner('Fetching API map stats');
914
- try {
915
- const data = await api.get('/api-map/stats');
916
- const stats = data.stats;
917
- s.stop('Stats loaded');
918
-
919
- if (flags.json) { ui.json(stats); return; }
920
-
921
- if (!stats) {
922
- console.log('');
923
- ui.info('No API map stats available.');
924
- console.log('');
925
- return;
926
- }
927
-
928
- console.log('');
929
- ui.heading('API Map Statistics');
930
- console.log('');
931
- ui.keyValue([
932
- ['Total Apps', String(stats.totalApps ?? '—')],
933
- ['Total Endpoints', String(stats.totalEndpoints ?? '—')],
934
- ['Total Requests', String(stats.totalRequests ?? '—')],
935
- ['Discovery Status', stats.discoveryStatus || '—'],
936
- ['Last Discovered', stats.lastDiscoveredAt ? new Date(stats.lastDiscoveredAt).toLocaleString() : '—'],
937
- ['Version', String(stats.version ?? '—')],
938
- ]);
939
- console.log('');
940
- } catch (err) {
941
- s.fail('Failed to fetch stats');
942
- throw err;
943
- }
944
- }
945
-
946
864
  // ── Instances ──
947
865
 
948
866
  async function instancesList(args, flags) {
@@ -1056,8 +974,6 @@ module.exports = {
1056
974
  forensicsLibrary,
1057
975
  ipLookup,
1058
976
  ipTraces,
1059
- apiMapList,
1060
- apiMapStats,
1061
977
  instancesList,
1062
978
  instancesTest,
1063
979
  analytics,
package/cli.js CHANGED
@@ -95,16 +95,6 @@ const COMMANDS = {
95
95
  },
96
96
  defaultSub: 'list',
97
97
  },
98
- issues: {
99
- desc: 'Manage security issues',
100
- usage: 'securenow issues <subcommand> [options]',
101
- sub: {
102
- list: { desc: 'List issues', flags: { app: 'App key', status: 'Filter by status' }, run: (a, f) => require('./cli/monitor').issuesList(a, f) },
103
- show: { desc: 'Show issue details', usage: 'securenow issues show <id>', run: (a, f) => require('./cli/monitor').issuesShow(a, f) },
104
- resolve: { desc: 'Resolve an issue', usage: 'securenow issues resolve <id>', run: (a, f) => require('./cli/monitor').issuesResolve(a, f) },
105
- },
106
- defaultSub: 'list',
107
- },
108
98
  notifications: {
109
99
  desc: 'Manage notifications',
110
100
  usage: 'securenow notifications <subcommand> [options]',
@@ -212,15 +202,6 @@ const COMMANDS = {
212
202
  },
213
203
  defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
214
204
  },
215
- 'api-map': {
216
- desc: 'View API map',
217
- usage: 'securenow api-map [stats]',
218
- sub: {
219
- list: { desc: 'List discovered API endpoints', run: (a, f) => require('./cli/security').apiMapList(a, f) },
220
- stats: { desc: 'API map statistics', run: (a, f) => require('./cli/security').apiMapStats(a, f) },
221
- },
222
- defaultSub: 'list',
223
- },
224
205
  instances: {
225
206
  desc: 'Manage ClickHouse instances',
226
207
  usage: 'securenow instances <subcommand> [options]',
@@ -362,8 +343,8 @@ function showHelp(commandName) {
362
343
  'Authentication': ['login', 'logout', 'whoami'],
363
344
  'Applications': ['apps', 'init', 'status'],
364
345
  'Observe': ['traces', 'logs', 'analytics'],
365
- 'Detect & Respond': ['issues', 'notifications', 'alerts', 'fp'],
366
- 'Investigate': ['ip', 'forensics', 'api-map'],
346
+ 'Detect & Respond': ['notifications', 'alerts', 'fp'],
347
+ 'Investigate': ['ip', 'forensics'],
367
348
  'Firewall': ['firewall'],
368
349
  'Remediation': ['blocklist', 'allowlist', 'trusted'],
369
350
  'Settings': ['instances', 'config', 'version'],
package/nextjs.js CHANGED
@@ -367,6 +367,44 @@ function registerSecureNow(options = {}) {
367
367
  },
368
368
  });
369
369
 
370
+ // -------- Guard against OTLP exporter socket errors --------
371
+ // The OTLP HTTP exporter uses keep-alive connections that can be reset by
372
+ // the remote end (ECONNRESET / "socket hang up"). These transient errors
373
+ // sometimes escape as unhandled exceptions or rejections. We catch them
374
+ // here and log at debug level instead of crashing the host app.
375
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
376
+ function _isOtlpTransientError(err) {
377
+ if (!err) return false;
378
+ if (_TRANSIENT_CODES.has(err.code)) return true;
379
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
380
+ return false;
381
+ }
382
+ function _looksLikeOtlpStack(err) {
383
+ const s = err && err.stack;
384
+ if (!s) return false;
385
+ return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
386
+ || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
387
+ }
388
+ const _diagDebug = (env('OTEL_LOG_LEVEL') || '').toLowerCase() === 'debug';
389
+ process.on('uncaughtException', (err, origin) => {
390
+ if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
391
+ if (_diagDebug) {
392
+ console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
393
+ }
394
+ return;
395
+ }
396
+ throw err;
397
+ });
398
+ process.on('unhandledRejection', (reason) => {
399
+ if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
400
+ if (_diagDebug) {
401
+ console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
402
+ }
403
+ return;
404
+ }
405
+ throw reason;
406
+ });
407
+
370
408
  if (isVercel) {
371
409
  // -------- Vercel Environment: Use @vercel/otel --------
372
410
  const { registerOTel } = require('@vercel/otel');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.17.1",
3
+ "version": "5.19.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
@@ -258,12 +258,12 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
258
258
  })();
259
259
 
260
260
  // -------- diagnostics --------
261
+ const diagLevel = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
261
262
  (() => {
262
- const L = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
263
- const level = L === 'debug' ? DiagLogLevel.DEBUG :
264
- L === 'info' ? DiagLogLevel.INFO :
265
- L === 'warn' ? DiagLogLevel.WARN :
266
- L === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
263
+ const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
264
+ diagLevel === 'info' ? DiagLogLevel.INFO :
265
+ diagLevel === 'warn' ? DiagLogLevel.WARN :
266
+ diagLevel === 'error' ? DiagLogLevel.ERROR : DiagLogLevel.NONE;
267
267
  diag.setLogger(new DiagConsoleLogger(), level);
268
268
  console.log('[securenow] preload loaded pid=%d', process.pid);
269
269
  })();
@@ -506,6 +506,48 @@ if (loggingEnabled) {
506
506
  console.__securenow_patched = true;
507
507
  }
508
508
 
509
+ // -------- Guard against OTLP exporter socket errors --------
510
+ // The OTLP HTTP exporter uses keep-alive connections that can be reset by the
511
+ // remote end (ECONNRESET / "socket hang up"). These transient errors sometimes
512
+ // escape as unhandled exceptions or rejections because the underlying HTTP
513
+ // request's error path isn't fully covered by the OTel library. We install
514
+ // targeted process-level handlers to catch them and log at debug level instead
515
+ // of crashing the host app.
516
+ const _TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN']);
517
+ function _isOtlpTransientError(err) {
518
+ if (!err) return false;
519
+ if (_TRANSIENT_CODES.has(err.code)) return true;
520
+ if (typeof err.message === 'string' && /socket hang up|ECONNRESET/.test(err.message)) return true;
521
+ return false;
522
+ }
523
+ function _looksLikeOtlpStack(err) {
524
+ const s = err && err.stack;
525
+ if (!s) return false;
526
+ return /OTLPTraceExporter|OTLPLogExporter|otlp|exporter.*http|BatchSpanProcessor|BatchLogRecordProcessor/i.test(s)
527
+ || /node:_http_client|ClientRequest|TLSSocket/i.test(s);
528
+ }
529
+
530
+ process.on('uncaughtException', (err, origin) => {
531
+ if (_isOtlpTransientError(err) && _looksLikeOtlpStack(err)) {
532
+ if (diagLevel === 'debug') {
533
+ console.debug('[securenow] Suppressed transient OTLP exporter error (%s): %s', origin, err.message);
534
+ }
535
+ return; // swallow — do not crash
536
+ }
537
+ // Not ours — re-throw so the default handler (or the app's own handler) fires
538
+ throw err;
539
+ });
540
+ process.on('unhandledRejection', (reason) => {
541
+ if (_isOtlpTransientError(reason) && _looksLikeOtlpStack(reason)) {
542
+ if (diagLevel === 'debug') {
543
+ console.debug('[securenow] Suppressed transient OTLP exporter rejection: %s', reason && reason.message);
544
+ }
545
+ return; // swallow
546
+ }
547
+ // Not ours — re-throw as unhandled so Node's default behaviour applies
548
+ throw reason;
549
+ });
550
+
509
551
  // -------- SDK --------
510
552
  const traceExporter = new OTLPTraceExporter({ url: tracesUrl, headers });
511
553
  const sdk = new NodeSDK({