securenow 5.18.0 → 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/cli/security.js +0 -84
- package/cli.js +1 -10
- package/nextjs.js +38 -0
- package/package.json +1 -1
- package/tracing.js +47 -5
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
|
@@ -202,15 +202,6 @@ const COMMANDS = {
|
|
|
202
202
|
},
|
|
203
203
|
defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
|
|
204
204
|
},
|
|
205
|
-
'api-map': {
|
|
206
|
-
desc: 'View API map',
|
|
207
|
-
usage: 'securenow api-map [stats]',
|
|
208
|
-
sub: {
|
|
209
|
-
list: { desc: 'List discovered API endpoints', run: (a, f) => require('./cli/security').apiMapList(a, f) },
|
|
210
|
-
stats: { desc: 'API map statistics', run: (a, f) => require('./cli/security').apiMapStats(a, f) },
|
|
211
|
-
},
|
|
212
|
-
defaultSub: 'list',
|
|
213
|
-
},
|
|
214
205
|
instances: {
|
|
215
206
|
desc: 'Manage ClickHouse instances',
|
|
216
207
|
usage: 'securenow instances <subcommand> [options]',
|
|
@@ -353,7 +344,7 @@ function showHelp(commandName) {
|
|
|
353
344
|
'Applications': ['apps', 'init', 'status'],
|
|
354
345
|
'Observe': ['traces', 'logs', 'analytics'],
|
|
355
346
|
'Detect & Respond': ['notifications', 'alerts', 'fp'],
|
|
356
|
-
'Investigate': ['ip', 'forensics'
|
|
347
|
+
'Investigate': ['ip', 'forensics'],
|
|
357
348
|
'Firewall': ['firewall'],
|
|
358
349
|
'Remediation': ['blocklist', 'allowlist', 'trusted'],
|
|
359
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
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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({
|