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 +3 -28
- package/README.md +1 -5
- package/SKILL-CLI.md +1 -11
- package/cli/monitor.js +0 -105
- package/cli/security.js +0 -84
- package/cli.js +2 -21
- package/nextjs.js +38 -0
- package/package.json +1 -1
- package/tracing.js +47 -5
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
|
|
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
|
|
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** | `
|
|
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
|
|
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
|
|
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': ['
|
|
366
|
-
'Investigate': ['ip', 'forensics'
|
|
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
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({
|