securenow 8.17.0 → 8.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/README.md +4 -1
- package/SKILL-CLI.md +75 -1
- package/cli/human.js +112 -1
- package/cli/monitor.js +43 -0
- package/cli/review.js +295 -0
- package/cli/security.js +121 -2
- package/cli.js +51 -10
- package/esm-detect.js +57 -0
- package/mcp/catalog.js +133 -20
- package/mcp/server.js +17 -2
- package/package.json +3 -2
- package/register.js +3 -7
- package/tracing.js +7 -13
package/cli/security.js
CHANGED
|
@@ -55,6 +55,9 @@ async function alertRulesRoute(args, flags) {
|
|
|
55
55
|
if (sub === 'from-text' || sub === 'draft') {
|
|
56
56
|
return alertRuleFromText(args.slice(1), flags);
|
|
57
57
|
}
|
|
58
|
+
if (sub === 'change') {
|
|
59
|
+
return alertRuleChange(args.slice(1), flags);
|
|
60
|
+
}
|
|
58
61
|
if (sub === 'show') {
|
|
59
62
|
return alertRuleShow(args.slice(1), flags);
|
|
60
63
|
}
|
|
@@ -94,6 +97,9 @@ async function alertRulesRoute(args, flags) {
|
|
|
94
97
|
if (sub === 'runs') {
|
|
95
98
|
return alertRuleRuns(args.slice(1), flags);
|
|
96
99
|
}
|
|
100
|
+
if (sub === 'request-promotion') {
|
|
101
|
+
return alertRuleRequestPromotion(args.slice(1), flags);
|
|
102
|
+
}
|
|
97
103
|
if (sub === 'run' || sub === 'run-now') {
|
|
98
104
|
return alertRuleRunNow(args.slice(1), flags);
|
|
99
105
|
}
|
|
@@ -472,9 +478,14 @@ async function alertRuleUpdate(args, flags) {
|
|
|
472
478
|
|
|
473
479
|
// Lifecycle mode promote/demote (also available as: alerts rules promote|demote <id>).
|
|
474
480
|
if (flags.mode === 'test' || flags.mode === 'prod') body.mode = flags.mode;
|
|
481
|
+
if (flags.severity) {
|
|
482
|
+
const severity = String(flags.severity).toLowerCase();
|
|
483
|
+
if (!['critical', 'high', 'medium', 'low'].includes(severity)) { ui.error('--severity must be critical, high, medium or low'); process.exit(1); }
|
|
484
|
+
body.severity = severity;
|
|
485
|
+
}
|
|
475
486
|
|
|
476
487
|
if (Object.keys(body).length === 0) {
|
|
477
|
-
ui.error('Nothing to update. Use --applications-all/--apps, --status (or --enable/--disable/--pause), or --mode test|prod (or: alerts rules promote/demote <id>).');
|
|
488
|
+
ui.error('Nothing to update. Use --applications-all/--apps, --status (or --enable/--disable/--pause), --severity, or --mode test|prod (or: alerts rules promote/demote <id>).');
|
|
478
489
|
process.exit(1);
|
|
479
490
|
}
|
|
480
491
|
|
|
@@ -482,7 +493,7 @@ async function alertRuleUpdate(args, flags) {
|
|
|
482
493
|
try {
|
|
483
494
|
await api.put(`/alert-rules/${id}`, body);
|
|
484
495
|
s.stop('Updated');
|
|
485
|
-
const extras = [body.status && `status → ${body.status}`, body.mode && `mode → ${body.mode}`].filter(Boolean);
|
|
496
|
+
const extras = [body.status && `status → ${body.status}`, body.mode && `mode → ${body.mode}`, body.severity && `severity → ${body.severity}`].filter(Boolean);
|
|
486
497
|
ui.success('Alert rule updated' + (extras.length ? ` (${extras.join(', ')})` : ''));
|
|
487
498
|
} catch (err) {
|
|
488
499
|
s.fail('Failed to update alert rule');
|
|
@@ -873,6 +884,87 @@ async function alertRuleFromText(args, flags) {
|
|
|
873
884
|
console.log(ui.c.dim(` Runs: securenow alerts rules runs ${id} Why no alert: securenow alerts rules why ${id} --ip <ip>`));
|
|
874
885
|
}
|
|
875
886
|
|
|
887
|
+
// Describe a change to an existing rule in English: the API rewrites the rule from its current
|
|
888
|
+
// version and the app's data, checks the result, and the change is applied as a new rule version.
|
|
889
|
+
async function alertRuleChange(args, flags) {
|
|
890
|
+
requireAuth();
|
|
891
|
+
const id = args[0];
|
|
892
|
+
const text = args.slice(1).join(' ').trim() || (typeof flags.text === 'string' ? flags.text.trim() : '');
|
|
893
|
+
if (!id || !text) {
|
|
894
|
+
ui.error('Usage: securenow alerts rules change <rule-id> "<what should change>" [--app <key>] [--yes] [--draft-only] [--json]');
|
|
895
|
+
console.log(ui.c.dim(' Example: securenow alerts rules change <id> "raise the threshold to 20 failed sign-ins"'));
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
const body = { text, ruleId: id };
|
|
899
|
+
if (flags.app) body.applicationKey = flags.app;
|
|
900
|
+
|
|
901
|
+
const s = ui.spinner('Rewriting the rule from your app\'s data');
|
|
902
|
+
let result;
|
|
903
|
+
try {
|
|
904
|
+
const started = await api.post('/alert-rules/draft', body);
|
|
905
|
+
result = await waitForDraft(started.draftId, s);
|
|
906
|
+
} catch (err) {
|
|
907
|
+
s.fail('Failed to draft the change');
|
|
908
|
+
throw err;
|
|
909
|
+
}
|
|
910
|
+
if (result.status !== 'complete') {
|
|
911
|
+
s.fail(result.error || 'Could not draft the change');
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
s.stop('Change drafted');
|
|
915
|
+
|
|
916
|
+
const matches = Number(result.check?.matches || 0);
|
|
917
|
+
const refuse = Boolean(flags.yes) && !flags.force && !flags['draft-only'] && matches > 0;
|
|
918
|
+
if (flags.json && (flags['draft-only'] || !flags.yes)) { ui.json(result); return; }
|
|
919
|
+
if (flags.json && refuse) {
|
|
920
|
+
ui.json({ ...result, applied: false, refused: 'check_matches' });
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
if (result.current?.kind === 'instant') {
|
|
924
|
+
console.log('');
|
|
925
|
+
console.log(ui.c.dim(` Before: ${result.current.matchMode === 'all' ? 'all of' : 'any of'} ${(result.current.conditions || []).map((c) => `${c.field} ${c.operator} ${c.value}`).join('; ')}`));
|
|
926
|
+
} else if (result.current?.sql) {
|
|
927
|
+
console.log('');
|
|
928
|
+
console.log(ui.c.dim(' Before'));
|
|
929
|
+
for (const line of String(result.current.sql).split('\n')) console.log(ui.c.dim(` ${line}`));
|
|
930
|
+
}
|
|
931
|
+
printDraft(result);
|
|
932
|
+
console.log('');
|
|
933
|
+
if (flags['draft-only']) { ui.info('Draft only: nothing was changed.'); return; }
|
|
934
|
+
if (refuse) {
|
|
935
|
+
ui.warn(`Not applied: the changed rule matches ${matches} row${matches === 1 ? '' : 's'} in your data right now, so it would alert at once. Apply anyway with --yes --force.`);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
if (!flags.yes) {
|
|
939
|
+
if (matches > 0) ui.warn(`The changed rule matches ${matches} row${matches === 1 ? '' : 's'} right now (above): it alerts as soon as it is applied.`);
|
|
940
|
+
const ok = await ui.confirm('Apply this change? It is saved as a new rule version (roll back with alerts rules rollback)');
|
|
941
|
+
if (!ok) { ui.info('Nothing changed'); return; }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const d = result.draft || {};
|
|
945
|
+
const update = {
|
|
946
|
+
name: d.name,
|
|
947
|
+
description: d.summary || undefined,
|
|
948
|
+
severity: d.severity,
|
|
949
|
+
reviewNote: `Changed in English: ${text}`.slice(0, 1000),
|
|
950
|
+
// Instant: only what the draft decides, so the rule keeps its own instant-block settings.
|
|
951
|
+
...(d.executionMode === 'instant'
|
|
952
|
+
? { instant: { matchMode: d.instant?.matchMode, conditions: d.instant?.conditions || [], environments: d.instant?.environments || [] } }
|
|
953
|
+
: { sqlQuery: d.sqlQuery, schedule: d.schedule }),
|
|
954
|
+
};
|
|
955
|
+
const c = ui.spinner('Applying the change');
|
|
956
|
+
try {
|
|
957
|
+
const data = await api.put(`/alert-rules/${id}`, update);
|
|
958
|
+
c.stop(`Changed: version ${data.configVersion || '?'}`);
|
|
959
|
+
printWarnings(data);
|
|
960
|
+
if (flags.json) ui.json(data);
|
|
961
|
+
else console.log(ui.c.dim(` History: securenow alerts rules versions ${id} Runs: securenow alerts rules runs ${id}`));
|
|
962
|
+
} catch (err) {
|
|
963
|
+
c.fail('Failed to apply the change');
|
|
964
|
+
throw err;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
876
968
|
async function alertRuleTest(args, flags) {
|
|
877
969
|
requireAuth();
|
|
878
970
|
const id = args[0];
|
|
@@ -1230,6 +1322,33 @@ async function alertRuleWhy(args, flags) {
|
|
|
1230
1322
|
console.log('');
|
|
1231
1323
|
}
|
|
1232
1324
|
|
|
1325
|
+
// An agent hands the promotion of its test-mode rule to a person: the request waits on the rules
|
|
1326
|
+
// page and in `securenow human promotions` until someone approves or denies it.
|
|
1327
|
+
async function alertRuleRequestPromotion(args, flags) {
|
|
1328
|
+
requireAuth();
|
|
1329
|
+
const id = args[0];
|
|
1330
|
+
const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
|
|
1331
|
+
if (!id || !reason) {
|
|
1332
|
+
ui.error('Usage: securenow alerts rules request-promotion <rule-id> --reason "why it is ready for prod" [--evidence "<text>|@file"]');
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
const body = { reason };
|
|
1336
|
+
if (typeof flags.evidence === 'string') {
|
|
1337
|
+
body.evidence = flags.evidence.startsWith('@') ? fs.readFileSync(flags.evidence.slice(1), 'utf8') : flags.evidence;
|
|
1338
|
+
}
|
|
1339
|
+
const s = ui.spinner('Asking for promotion');
|
|
1340
|
+
try {
|
|
1341
|
+
const data = await api.post(`/alert-rules/${id}/promotion-requests`, body);
|
|
1342
|
+
s.stop(data.existing ? 'A request for this rule is already waiting' : 'Promotion requested');
|
|
1343
|
+
if (flags.json) { ui.json(data); return; }
|
|
1344
|
+
ui.info(data.message || 'Waiting for a person to approve it.');
|
|
1345
|
+
if (data.request?.id) console.log(ui.c.dim(` Request ${data.request.id}. A person decides with: securenow human promotions`));
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
s.fail('Failed to request promotion');
|
|
1348
|
+
throw err;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1233
1352
|
// ── Alert Channels ──
|
|
1234
1353
|
|
|
1235
1354
|
function channelDestination(ch) {
|
package/cli.js
CHANGED
|
@@ -195,6 +195,7 @@ const COMMANDS = {
|
|
|
195
195
|
sub: {
|
|
196
196
|
list: { desc: 'List recent logs', flags: { app: 'App key', env: 'Environment (production, staging, preview, local, or all)', environment: 'Alias for --env', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
|
|
197
197
|
trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
|
|
198
|
+
errors: { desc: 'Error lines grouped by message (ids and numbers normalized), most frequent first', usage: 'securenow logs errors [--app <key>] [--env <env>] [--since 1h] [--warn] [--limit 20]', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env', since: 'Window, e.g. 30m, 6h, 2d (default 1h)', warn: 'Include warnings', limit: 'Max groups (default 20)', json: 'Output as JSON' }, run: (a, f) => require('./cli/monitor').logsErrors(a, f) },
|
|
198
199
|
},
|
|
199
200
|
defaultSub: 'list',
|
|
200
201
|
},
|
|
@@ -248,10 +249,11 @@ const COMMANDS = {
|
|
|
248
249
|
defaultSub: 'list',
|
|
249
250
|
},
|
|
250
251
|
human: {
|
|
251
|
-
desc: '
|
|
252
|
-
usage: 'securenow human <list|show|block|fp|report|action|prompt|work> [row|notificationId:ip] [options]',
|
|
252
|
+
desc: 'Requires Human: decisions SecureNow prepared but won\'t make alone (block an IP, false positive, close, approve a plan)',
|
|
253
|
+
usage: 'securenow human <list|show|block|fp|close|report|action|prompt|work|promotions|approve-promotion|deny-promotion> [row|notificationId:ip|request-id] [options]',
|
|
253
254
|
flags: {
|
|
254
255
|
json: 'Output as JSON',
|
|
256
|
+
note: 'With approve-promotion / deny-promotion: a note recorded with the decision',
|
|
255
257
|
page: 'Queue page number',
|
|
256
258
|
limit: 'Queue page size',
|
|
257
259
|
search: 'Search IP, rule, path, or verdict',
|
|
@@ -282,10 +284,40 @@ const COMMANDS = {
|
|
|
282
284
|
show: { desc: 'Show one row with AI report, proofs, investigation steps, and trace links', usage: 'securenow human show <row|notificationId:ip>', run: (a, f) => require('./cli/human').show(a, f) },
|
|
283
285
|
block: { desc: 'Approve the AI block recommendation for a row', usage: 'securenow human block <row|notificationId:ip> --yes --reason "..."', run: (a, f) => require('./cli/human').block(a, f) },
|
|
284
286
|
fp: { desc: 'Mark a row as a scoped false positive', usage: 'securenow human fp <row|notificationId:ip> --yes --reason "..."', run: (a, f) => require('./cli/human').fp(a, f) },
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
close: { desc: 'Close an IP row without blocking it (not now, rule needs tuning, rate-limited...): it leaves the queue, the reason stays on the case', usage: 'securenow human close <row|notificationId:ip> --outcome deferred|rule_tuning|new_alert_rule|ambiguous|skipped|rate_limited|other --reason "..." [--missing-proof "..."] --yes', run: (a, f) => require('./cli/human').close(a, f) },
|
|
288
|
+
report: { desc: 'Add a decision note to a row; the row stays in the queue (use close to remove it)', usage: 'securenow human report <row|notificationId:ip> --yes --outcome ambiguous --summary "..."', run: (a, f) => require('./cli/human').report(a, f) },
|
|
289
|
+
action: { desc: 'Approve/reject a case-level plan. approve_block blocks; approving tune_rule/create_exclusion only records it (change the rule first)', usage: 'securenow human action <row|notificationId> [actionKey] --status approved --yes --reason "..."', run: (a, f) => require('./cli/human').action(a, f) },
|
|
287
290
|
prompt: { desc: 'Print a Codex/Claude MCP prompt for row or queue work', usage: 'securenow human prompt [row|notificationId:ip] [--limit 10]', run: (a, f) => require('./cli/human').prompt(a, f) },
|
|
288
291
|
work: { desc: 'List the queue and print the MCP runbook to work it deeply', usage: 'securenow human work [--limit 10]', run: (a, f) => require('./cli/human').work(a, f) },
|
|
292
|
+
promotions: { desc: 'List requests to promote a test-mode rule to prod (agents ask, a person decides)', usage: 'securenow human promotions [--status pending|approved|denied|all]', run: (a, f) => require('./cli/human').promotions(a, f) },
|
|
293
|
+
'approve-promotion': { desc: 'Approve a promotion request: the rule goes to prod', usage: 'securenow human approve-promotion <request-id> --yes [--note "..."]', run: (a, f) => require('./cli/human').approvePromotion(a, f) },
|
|
294
|
+
'deny-promotion': { desc: 'Deny a promotion request: the rule stays in test mode', usage: 'securenow human deny-promotion <request-id> --reason "..."', run: (a, f) => require('./cli/human').denyPromotion(a, f) },
|
|
295
|
+
},
|
|
296
|
+
defaultSub: 'list',
|
|
297
|
+
},
|
|
298
|
+
review: {
|
|
299
|
+
desc: 'Alert Review: "is this rule right?" Rules that fired too wide or on your own infrastructure, paused until someone decides',
|
|
300
|
+
usage: 'securenow review <list|show|keep|close|dismiss|group> [row|rootCauseKey|notificationId] [options]',
|
|
301
|
+
flags: {
|
|
302
|
+
json: 'Output as JSON',
|
|
303
|
+
page: 'Groups page number',
|
|
304
|
+
limit: 'Groups page size',
|
|
305
|
+
search: 'Search rule, route, IP, or app name',
|
|
306
|
+
outcome: 'Why it closes: rule_tuning, false_positive, clean, deferred, new_alert_rule, rate_limited, ambiguous, skipped, other',
|
|
307
|
+
reason: 'Why, in one sentence: kept on the alert as its decision report',
|
|
308
|
+
summary: 'Longer decision summary (defaults to --reason)',
|
|
309
|
+
preview: 'With group: show what would close, change nothing',
|
|
310
|
+
'allow-mixed': 'With group: close a group whose rows disagree, after reviewing every row',
|
|
311
|
+
yes: 'Confirm without prompting',
|
|
312
|
+
force: 'Alias for --yes',
|
|
313
|
+
},
|
|
314
|
+
sub: {
|
|
315
|
+
list: { desc: 'List open alert runs grouped by root cause (one decision can close a group)', usage: 'securenow review list [--search lab-auth]', run: (a, f) => require('./cli/review').list(a, f) },
|
|
316
|
+
show: { desc: 'Show a group (row or arc_ key) or one alert run (notification id)', usage: 'securenow review show <row|rootCauseKey|notificationId>', run: (a, f) => require('./cli/review').show(a, f) },
|
|
317
|
+
keep: { desc: 'The rule is right: turn it back on, stop it pausing itself, close the run', usage: 'securenow review keep <notificationId> --reason "..." --yes', run: (a, f) => require('./cli/review').keep(a, f) },
|
|
318
|
+
close: { desc: 'Close one run with an outcome; nothing is blocked or unblocked', usage: 'securenow review close <notificationId> --outcome rule_tuning --reason "..." --yes', run: (a, f) => require('./cli/review').close(a, f) },
|
|
319
|
+
dismiss: { desc: 'Dismiss one run (not worth acting on); nothing is blocked or unblocked', usage: 'securenow review dismiss <notificationId> --reason "..." --yes', run: (a, f) => require('./cli/review').dismiss(a, f) },
|
|
320
|
+
group: { desc: 'Close every run in a group with one decision; --preview first', usage: 'securenow review group <row|rootCauseKey> --outcome rule_tuning --reason "..." [--preview] [--allow-mixed] --yes', run: (a, f) => require('./cli/review').group(a, f) },
|
|
289
321
|
},
|
|
290
322
|
defaultSub: 'list',
|
|
291
323
|
},
|
|
@@ -314,9 +346,11 @@ const COMMANDS = {
|
|
|
314
346
|
desc: 'Create (from English or SQL), list, show, update, duplicate, promote/demote, set-sql, validate, delete, test, run, explain, or tune alert rules',
|
|
315
347
|
usage: 'securenow alerts rules <list|from-text|create|show|update|duplicate|promote|demote|set-sql|validate|delete|test|run|runs|why|dry-run-query|tune-query|exclusions|versions|version|rollback> [options]\n'
|
|
316
348
|
+ ' securenow alerts rules from-text "<what to catch, in English>" --app <key> [--env <environment>] [--instant|--periodic] [--yes|--draft-only]\n'
|
|
349
|
+
+ ' securenow alerts rules change <id> "<what should change>" [--yes|--draft-only] rewrite a rule from English, checked, applied as a new version\n'
|
|
317
350
|
+ ' securenow alerts rules runs <id> [--limit 20] [--since 1h] what each run saw and why it did or didn\'t notify\n'
|
|
318
351
|
+ ' securenow alerts rules run <id> [--no-wait] run a periodic rule now\n'
|
|
319
|
-
+ ' securenow alerts rules why <id> --ip <ip> [--since 1h] why the rule did or didn\'t alert on one address'
|
|
352
|
+
+ ' securenow alerts rules why <id> --ip <ip> [--since 1h] why the rule did or didn\'t alert on one address\n'
|
|
353
|
+
+ ' securenow alerts rules request-promotion <id> --reason "..." ask a person to promote a test-mode rule (for agents)',
|
|
320
354
|
flags: {
|
|
321
355
|
json: 'Output as JSON',
|
|
322
356
|
env: 'With from-text or create --conditions: only watch this environment (default: every environment)',
|
|
@@ -330,7 +364,7 @@ const COMMANDS = {
|
|
|
330
364
|
nlp: 'With create: plain-English intent stored on the query',
|
|
331
365
|
text: 'Alias for --nlp',
|
|
332
366
|
category: 'With create: query category (default: custom)',
|
|
333
|
-
severity: 'With create: critical | high | medium | low',
|
|
367
|
+
severity: 'With create/update: critical | high | medium | low',
|
|
334
368
|
schedule: 'With create: cron expression (default */15 * * * *)',
|
|
335
369
|
'throttle-minutes': 'With create: one alert per address every N minutes (default 15)',
|
|
336
370
|
'no-throttle': 'With create: disable throttling',
|
|
@@ -359,7 +393,8 @@ const COMMANDS = {
|
|
|
359
393
|
query: 'Alias for --sql',
|
|
360
394
|
file: 'Read SQL from a file',
|
|
361
395
|
'no-validate': 'With create/set-sql: skip the post-write dry-run + rollback',
|
|
362
|
-
reason: 'Audit reason for a write',
|
|
396
|
+
reason: 'Audit reason for a write. With request-promotion: why the rule is ready for prod',
|
|
397
|
+
evidence: 'With request-promotion: evidence text or @file (runs, notifications, false-positive checks)',
|
|
363
398
|
'apply-globally': 'Required for system query tuning',
|
|
364
399
|
'reactivate-paused': 'Reactivate paused system copies after tuning',
|
|
365
400
|
'expected-hash': 'Expected current SQL SHA-256 hash',
|
|
@@ -775,8 +810,14 @@ const COMMANDS = {
|
|
|
775
810
|
},
|
|
776
811
|
mcp: {
|
|
777
812
|
desc: 'Start the SecureNow MCP server over stdio',
|
|
778
|
-
usage: 'securenow mcp',
|
|
779
|
-
|
|
813
|
+
usage: 'securenow mcp [--profile all|investigator|detection|responder]',
|
|
814
|
+
flags: {
|
|
815
|
+
profile: 'Serve a subset of the tools: investigator (read-only investigation), detection (alert rules), responder (alerts, blocks, limits). Default: all',
|
|
816
|
+
},
|
|
817
|
+
run: (a, f) => {
|
|
818
|
+
if (typeof f.profile === 'string') process.env.SECURENOW_MCP_PROFILE = f.profile;
|
|
819
|
+
require('./mcp/server');
|
|
820
|
+
},
|
|
780
821
|
},
|
|
781
822
|
version: {
|
|
782
823
|
desc: 'Show CLI version',
|
|
@@ -840,7 +881,7 @@ function showHelp(commandName) {
|
|
|
840
881
|
'Authentication': ['login', 'logout', 'whoami', 'credentials'],
|
|
841
882
|
'Applications': ['apps', 'init', 'status'],
|
|
842
883
|
'Observe': ['traces', 'logs', 'analytics'],
|
|
843
|
-
'Detect & Respond': ['human', 'notifications', 'alerts', 'fp'],
|
|
884
|
+
'Detect & Respond': ['human', 'review', 'notifications', 'alerts', 'fp'],
|
|
844
885
|
'Investigate': ['ip', 'forensics'],
|
|
845
886
|
'Firewall': ['firewall'],
|
|
846
887
|
'Remediation': ['automation', 'ratelimit', 'challenge', 'blocklist', 'revoke', 'allowlist', 'trusted'],
|
package/esm-detect.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Is this an ESM app, so the preload must register the ESM loader hook?
|
|
5
|
+
*
|
|
6
|
+
* Node decides a file's module type from the nearest package.json above THE FILE, not from
|
|
7
|
+
* the working directory. Process managers often start apps from another directory (pm2 `cwd`,
|
|
8
|
+
* systemd WorkingDirectory, a Docker WORKDIR, a supervisor that runs each environment from its
|
|
9
|
+
* own folder). Checking only process.cwd() then missed the hook, and every library the app
|
|
10
|
+
* imports (Express, pg, ...) went uninstrumented: no http.route on any request span.
|
|
11
|
+
*
|
|
12
|
+
* ESM when any of these says so: the entry script is .mjs; the nearest package.json above the
|
|
13
|
+
* entry script (process.argv[1], or pm2's pm_exec_path) has "type": "module"; or the working
|
|
14
|
+
* directory's package.json does (the original check, kept for launchers whose argv[1] is a wrapper).
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
function readType(pkgPath) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8').replace(/^/, '')).type || 'commonjs';
|
|
22
|
+
} catch (_) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nearestPackageJson(startDir) {
|
|
28
|
+
let dir = startDir;
|
|
29
|
+
for (let i = 0; i < 64; i++) {
|
|
30
|
+
const candidate = path.join(dir, 'package.json');
|
|
31
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir) return null;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @returns {{ esm: boolean, reason: string|null }} */
|
|
40
|
+
function detectEsmApp({ argv = process.argv, cwd = process.cwd(), env = process.env } = {}) {
|
|
41
|
+
const entries = [argv && argv[1], env && env.pm_exec_path]
|
|
42
|
+
.filter((p) => typeof p === 'string' && p && !p.startsWith('-'))
|
|
43
|
+
.map((p) => path.resolve(cwd, p));
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (/\.mjs$/i.test(entry)) return { esm: true, reason: `${path.basename(entry)} is an .mjs file` };
|
|
47
|
+
if (/\.cjs$/i.test(entry)) continue;
|
|
48
|
+
const pkgPath = nearestPackageJson(path.dirname(entry));
|
|
49
|
+
if (pkgPath && readType(pkgPath) === 'module') return { esm: true, reason: `${pkgPath} has "type": "module"` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cwdPkg = path.resolve(cwd, 'package.json');
|
|
53
|
+
if (fs.existsSync(cwdPkg) && readType(cwdPkg) === 'module') return { esm: true, reason: `${cwdPkg} has "type": "module"` };
|
|
54
|
+
return { esm: false, reason: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { detectEsmApp, nearestPackageJson };
|