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/mcp/catalog.js
CHANGED
|
@@ -353,7 +353,7 @@ const TOOLS = [
|
|
|
353
353
|
{
|
|
354
354
|
name: 'securenow_traces_show',
|
|
355
355
|
title: 'Show Trace',
|
|
356
|
-
description: 'Show spans for a trace id scoped to one or more app keys.',
|
|
356
|
+
description: 'Show spans for a trace id scoped to one or more app keys. Fields identical on every span (service, environment and other resource attributes) come once under shared; empty fields are left out.',
|
|
357
357
|
scope: 'traces:read',
|
|
358
358
|
readOnly: true,
|
|
359
359
|
method: 'GET',
|
|
@@ -385,6 +385,25 @@ const TOOLS = [
|
|
|
385
385
|
limit: number('Maximum logs to return.', { minimum: 1, maximum: 500 }),
|
|
386
386
|
}, ['appKeys']),
|
|
387
387
|
},
|
|
388
|
+
{
|
|
389
|
+
name: 'securenow_logs_errors',
|
|
390
|
+
title: 'Summarize Error Logs',
|
|
391
|
+
description: 'What is failing: error (and fatal) log lines grouped by message, with ids, uuids, IPs and numbers normalized so repeats are one group. Each group: a sample line, how many times, first and last seen, environments, exception type and sample trace ids (pass one to securenow_logs_for_trace or securenow_traces_show). Far cheaper than listing raw logs. includeWarn adds warnings.',
|
|
392
|
+
scope: 'logs:read',
|
|
393
|
+
readOnly: true,
|
|
394
|
+
method: 'GET',
|
|
395
|
+
endpoint: '/logs/errors',
|
|
396
|
+
queryFields: ['appKeys', 'environment', 'since', 'from', 'to', 'includeWarn', 'limit'],
|
|
397
|
+
normalize: { appKeys: normalizeAppKeys },
|
|
398
|
+
inputSchema: objectSchema({
|
|
399
|
+
...appKeysInput,
|
|
400
|
+
...environmentInput,
|
|
401
|
+
since: string('Window: 30m, 1h (default), 6h, 2d; or pass from/to.'),
|
|
402
|
+
...timeRangeInput,
|
|
403
|
+
includeWarn: boolean('Also group warnings.'),
|
|
404
|
+
limit: number('Maximum groups (default 20).', { minimum: 1, maximum: 100 }),
|
|
405
|
+
}, ['appKeys']),
|
|
406
|
+
},
|
|
388
407
|
{
|
|
389
408
|
name: 'securenow_logs_for_trace',
|
|
390
409
|
title: 'Logs For Trace',
|
|
@@ -513,7 +532,7 @@ const TOOLS = [
|
|
|
513
532
|
rootCauseKey: string('Optional root-cause group key to filter to one group.'),
|
|
514
533
|
groupKey: string('Compatibility alias for rootCauseKey.'),
|
|
515
534
|
includeRuns: boolean('Whether to include compact matching run rows in the response.'),
|
|
516
|
-
maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to
|
|
535
|
+
maxScan: number('Maximum pending-review rows to scan before grouping. Defaults to 1000 (the dashboard count), the maximum.', { minimum: 1, maximum: 1000 }),
|
|
517
536
|
}),
|
|
518
537
|
},
|
|
519
538
|
{
|
|
@@ -728,7 +747,7 @@ const TOOLS = [
|
|
|
728
747
|
{
|
|
729
748
|
name: 'securenow_human_action_close_no_write',
|
|
730
749
|
title: 'Close Human Action Without Enforcement',
|
|
731
|
-
description: 'Close one Requires Human IP row with an auditable
|
|
750
|
+
description: 'Close one Requires Human IP row without blocking or marking it a false positive, with an auditable decision report (outcome: rate_limited, deferred, rule_tuning, new_alert_rule, ambiguous, skipped or other). This is how a "not now / not enforceable" decision leaves the queue; a decision report on its own does not.',
|
|
732
751
|
scope: 'notifications:write',
|
|
733
752
|
readOnly: false,
|
|
734
753
|
confirm: true,
|
|
@@ -773,7 +792,7 @@ const TOOLS = [
|
|
|
773
792
|
{
|
|
774
793
|
name: 'securenow_human_case_action_update',
|
|
775
794
|
title: 'Update Case-Level Human Action',
|
|
776
|
-
description: 'Approve, reject, execute, or fail a case-level proposed action such as tune_rule or create_exclusion. Write action; requires confirmation.',
|
|
795
|
+
description: 'Approve, reject, execute, or fail a case-level proposed action such as tune_rule or create_exclusion. Approving approve_block blocks the IPs and investigate_cluster starts investigations; approving tune_rule, create_exclusion or review_protected_ip only records the decision (apply the rule change first). Write action; requires confirmation.',
|
|
777
796
|
scope: 'notifications:write',
|
|
778
797
|
readOnly: false,
|
|
779
798
|
confirm: true,
|
|
@@ -1381,13 +1400,13 @@ const TOOLS = [
|
|
|
1381
1400
|
{
|
|
1382
1401
|
name: 'securenow_alert_rule_draft',
|
|
1383
1402
|
title: 'Draft an Alert Rule From English',
|
|
1384
|
-
description: 'Turn a detection idea in plain English into an alert rule draft for one app, checked against that app\'s data. It reads the app\'s real event types, attribute keys and routes, then picks the kind: instant (conditions checked on every incoming request, alerts within seconds; one request is enough evidence) or periodic (SQL run on a schedule: counts, thresholds, distinct counts, ratios, sequences, baselines, security events, app log lines). Periodic SQL is run once against the data; instant conditions are replayed over the app\'s last 2000 requests, so check.matches shows what would alert today (false positives). Nothing is saved: create it with securenow_alert_rule_create, passing the draft fields. Waits up to 120 seconds.',
|
|
1403
|
+
description: 'Turn a detection idea in plain English into an alert rule draft for one app, checked against that app\'s data. It reads the app\'s real event types, attribute keys and routes, then picks the kind: instant (conditions checked on every incoming request, alerts within seconds; one request is enough evidence) or periodic (SQL run on a schedule: counts, thresholds, distinct counts, ratios, sequences, baselines, security events, app log lines). Periodic SQL is run once against the data; instant conditions are replayed over the app\'s last 2000 requests, so check.matches shows what would alert today (false positives). Nothing is saved: create it with securenow_alert_rule_create, passing the draft fields. To CHANGE an existing rule, pass ruleId and describe the change in text: the draft is the whole rule rewritten (same kind, the rest kept), with `current` showing the rule as it is; apply it with securenow_alert_rule_update (sqlQuery + schedule, or instant {matchMode, conditions, environments}). Waits up to 120 seconds.',
|
|
1385
1404
|
scope: 'alerts:write',
|
|
1386
1405
|
readOnly: false,
|
|
1387
1406
|
confirm: true,
|
|
1388
1407
|
method: 'POST',
|
|
1389
1408
|
endpoint: '/alert-rules/draft',
|
|
1390
|
-
bodyFields: ['text', 'applicationKey', 'environment', 'kind'],
|
|
1409
|
+
bodyFields: ['text', 'applicationKey', 'environment', 'kind', 'ruleId'],
|
|
1391
1410
|
poll: {
|
|
1392
1411
|
tool: 'securenow_alert_rule_draft_status',
|
|
1393
1412
|
idField: 'draftId',
|
|
@@ -1400,8 +1419,9 @@ const TOOLS = [
|
|
|
1400
1419
|
applicationKey: string('App key the rule watches; the draft is checked against its data.'),
|
|
1401
1420
|
environment: string('Only watch this environment (e.g. production). Omit to watch every environment.'),
|
|
1402
1421
|
kind: string('Optional: instant or periodic. Omit to let the drafter choose.'),
|
|
1422
|
+
ruleId: string('Optional: an existing custom rule to change; text then describes the change. applicationKey defaults to one the rule watches.'),
|
|
1403
1423
|
...confirmSchema,
|
|
1404
|
-
}, ['text', '
|
|
1424
|
+
}, ['text', 'confirm', 'reason']),
|
|
1405
1425
|
},
|
|
1406
1426
|
{
|
|
1407
1427
|
name: 'securenow_alert_rule_draft_status',
|
|
@@ -1521,23 +1541,26 @@ const TOOLS = [
|
|
|
1521
1541
|
{
|
|
1522
1542
|
name: 'securenow_alert_rule_update',
|
|
1523
1543
|
title: 'Update Alert Rule',
|
|
1524
|
-
description: 'Update alert rule scope/status/schedule/throttle or record a review action. Write action; requires confirmation.',
|
|
1544
|
+
description: 'Update alert rule scope/status/severity/schedule/throttle, a custom rule\'s SQL (sqlQuery, checked like create: scoped, time-bounded, with an ip column) or instant conditions (instant: {matchMode, conditions, environments}; omitted fields such as the instant block stay as they are), or record a review action. reviewNote is kept as the version\'s reason. Write action; requires confirmation.',
|
|
1525
1545
|
scope: 'alerts:write',
|
|
1526
1546
|
readOnly: false,
|
|
1527
1547
|
confirm: true,
|
|
1528
1548
|
method: 'PUT',
|
|
1529
1549
|
endpoint: '/alert-rules/:id',
|
|
1530
1550
|
pathParams: ['id'],
|
|
1531
|
-
bodyFields: ['name', 'description', 'status', 'applicationsAll', 'applications', 'schedule', 'throttle', 'alertChannelIds', 'reviewAction', 'reviewNote', 'reviewNotificationId'],
|
|
1551
|
+
bodyFields: ['name', 'description', 'severity', 'status', 'applicationsAll', 'applications', 'schedule', 'throttle', 'sqlQuery', 'instant', 'alertChannelIds', 'reviewAction', 'reviewNote', 'reviewNotificationId'],
|
|
1532
1552
|
inputSchema: objectSchema({
|
|
1533
1553
|
id: string('Alert rule id.'),
|
|
1534
1554
|
name: string('Rule name for custom rules.'),
|
|
1535
1555
|
description: string('Rule description for custom rules.'),
|
|
1556
|
+
severity: string('critical, high, medium or low (custom rules).'),
|
|
1536
1557
|
status: string('Active, Disabled, or Paused.'),
|
|
1537
1558
|
applicationsAll: boolean('Scope rule to all applications.'),
|
|
1538
1559
|
applications: arrayOfStrings('Application keys when applicationsAll is false.'),
|
|
1539
1560
|
schedule: { type: 'object', additionalProperties: true, description: 'Schedule patch.' },
|
|
1540
1561
|
throttle: { type: 'object', additionalProperties: true, description: 'Throttle patch {enabled, minutes}: one alert per address every N minutes.' },
|
|
1562
|
+
sqlQuery: string('Custom periodic rules: the replacement detection SELECT (e.g. from a securenow_alert_rule_draft with ruleId).'),
|
|
1563
|
+
instant: { type: 'object', additionalProperties: true, description: 'Custom instant rules: {matchMode, conditions: [{field, operator, value}], environments}. Fields left out keep their current values.' },
|
|
1541
1564
|
alertChannelIds: arrayOfStrings('Alert channel ids.'),
|
|
1542
1565
|
reviewAction: string('Rule review action: keep_active (keep it running and stop auto-pausing it) or tuned_version (the rule was tuned; resume it).'),
|
|
1543
1566
|
reviewNote: string('Review note.'),
|
|
@@ -1928,6 +1951,37 @@ const TOOLS = [
|
|
|
1928
1951
|
since: string('Window start: 30m, 1h (default), 2d or an ISO date; at most 7 days.'),
|
|
1929
1952
|
}, ['id', 'ip']),
|
|
1930
1953
|
},
|
|
1954
|
+
{
|
|
1955
|
+
name: 'securenow_alert_rule_request_promotion',
|
|
1956
|
+
title: 'Ask a Person to Promote an Alert Rule',
|
|
1957
|
+
description: 'Ask a person to promote a test-mode rule to prod, with why it is ready and the evidence (runs, notifications it caught, false-positive checks). Agents create rules in test mode and cannot approve this themselves: a person approves on the rules page or with `securenow human approve-promotion`, which moves the rule to prod (its instant block, matching automations and the AI pipeline can then act on its alerts). One pending request per rule. Write action; requires confirmation.',
|
|
1958
|
+
scope: 'alerts:write',
|
|
1959
|
+
readOnly: false,
|
|
1960
|
+
confirm: true,
|
|
1961
|
+
method: 'POST',
|
|
1962
|
+
endpoint: '/alert-rules/:id/promotion-requests',
|
|
1963
|
+
pathParams: ['id'],
|
|
1964
|
+
bodyFields: ['reason', 'evidence'],
|
|
1965
|
+
inputSchema: objectSchema({
|
|
1966
|
+
id: string('Alert rule id (a custom rule in test mode).'),
|
|
1967
|
+
evidence: string('What shows it is ready: e.g. runs and their outcomes, notifications it raised on real attacks, how many false positives in how long.'),
|
|
1968
|
+
...confirmSchema,
|
|
1969
|
+
}, ['id', 'confirm', 'reason']),
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
name: 'securenow_alert_rule_promotion_requests',
|
|
1973
|
+
title: 'List Alert Rule Promotion Requests',
|
|
1974
|
+
description: 'Requests to promote test-mode rules to prod and their status (pending, approved, denied), who asked and why, and who decided.',
|
|
1975
|
+
scope: 'alerts:read',
|
|
1976
|
+
readOnly: true,
|
|
1977
|
+
method: 'GET',
|
|
1978
|
+
endpoint: '/alert-rules/promotion-requests',
|
|
1979
|
+
queryFields: ['status', 'limit'],
|
|
1980
|
+
inputSchema: objectSchema({
|
|
1981
|
+
status: string('pending (default), approved, denied or all.'),
|
|
1982
|
+
limit: number('How many (default 50, max 100).', { minimum: 1, maximum: 100 }),
|
|
1983
|
+
}),
|
|
1984
|
+
},
|
|
1931
1985
|
{
|
|
1932
1986
|
name: 'securenow_alert_rule_exclusions_list',
|
|
1933
1987
|
title: 'List Alert Rule Exclusions',
|
|
@@ -2409,10 +2463,11 @@ const PROMPTS = [
|
|
|
2409
2463
|
{
|
|
2410
2464
|
name: 'soc_patrol',
|
|
2411
2465
|
title: 'SOC Patrol (Scheduled)',
|
|
2412
|
-
description: '
|
|
2466
|
+
description: 'One bounded pass over both review queues, meant to run on a schedule from your own agent (Claude Code, Codex, Cursor, ...): Alert Review rule fixes first, then the Requires Human queue.',
|
|
2413
2467
|
arguments: [
|
|
2414
2468
|
{ name: 'maxAlertReviewGroups', description: 'Maximum alert-review root-cause groups to work this pass. Defaults to 10.', required: false },
|
|
2415
2469
|
{ name: 'maxHumanRows', description: 'Maximum Requires Human rows to work this pass. Defaults to 10.', required: false },
|
|
2470
|
+
{ name: 'search', description: 'Optional filter applied to both queues (an app name, rule, route or IP), e.g. to let a pass write only on one app.', required: false },
|
|
2416
2471
|
{ name: 'confirmWrites', description: 'Set true only when this scheduled run is allowed to execute decisions autonomously.', required: false },
|
|
2417
2472
|
],
|
|
2418
2473
|
},
|
|
@@ -2541,7 +2596,8 @@ function promptMessages(name, args = {}) {
|
|
|
2541
2596
|
`Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
|
|
2542
2597
|
'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/investigation steps/proofs/trace IDs, and fetch trace/log evidence where useful.',
|
|
2543
2598
|
'For each row choose exactly one outcome: Block IP, Rate Limit, False Positive, Rule Tuning Applied/Needed, New Alert Rule Needed, Case Action Approved/Rejected, or Ambiguous/Skipped with exact missing proof.',
|
|
2544
|
-
'
|
|
2599
|
+
'A row leaves the queue only when its status changes: a block, a false positive, an approved or rejected case action, or securenow_human_action_close_no_write (closes an IP row without enforcement, with an outcome and the exact proof gap, missing permission or vague guard). A decision report alone (securenow_human_action_decision_report_add) only adds a note: the row stays open.',
|
|
2600
|
+
'Approving a tune_rule, create_exclusion or review_protected_ip case action records the decision and closes the row; it does not change the rule by itself. Apply the change first (rule tools), then approve. Reject a plan that is vague or wrong.',
|
|
2545
2601
|
'If a global/system rule update is the right fix but access is denied, do not mark customer-specific false positives just to clear the row. Prepare the exact attack-preserving guard/SQL, dry-run it when possible, and record outcome=rule_tuning with missingProof.',
|
|
2546
2602
|
'If evidence is mixed, do partial resolution: block proven malicious IPs, false-positive exact benign shapes, rate-limit route-specific volume abuse, and leave ambiguous IPs with their missing proof.',
|
|
2547
2603
|
'Reject vague AI proposed actions. If the prompt/action lacks concrete rule id, path, method, status, user-agent/body/header guard, benign samples, and attack examples that still match, record it as ambiguous or rule_tuning needed.',
|
|
@@ -2549,9 +2605,9 @@ function promptMessages(name, args = {}) {
|
|
|
2549
2605
|
'Do not rate-limit confirmed high-risk attacks that should be blocked, benign traffic that should be false-positive scoped, broad noisy rules that need tuning, app-server/proxy attribution problems, or isolated one-off requests.',
|
|
2550
2606
|
'Do not create or recommend IP Allowlist entries while working this queue unless the user explicitly approves deny-by-default allowlist mode for the whole app/environment. Use Trusted IPs for trusted/bypass cases.',
|
|
2551
2607
|
confirmWrites
|
|
2552
|
-
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For skipped
|
|
2553
|
-
: 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
2554
|
-
'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then
|
|
2608
|
+
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant, then continue. For IP rows you decide not to enforce (skipped, ambiguous, rule tuning needed, new rule needed, already rate-limited), close them with securenow_human_action_close_no_write and that outcome so they leave the queue.'
|
|
2609
|
+
: 'Do not execute write tools yet. Read-only tools are fine, and so are the dry-run test tools (securenow_alert_rule_candidate_test, securenow_alert_rule_condition_candidate_test, securenow_alert_rule_condition_diff): they change nothing (pass confirm:true; they need an alerts:write key, so on a read-only key mark the guard untested). Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
2610
|
+
'For block decisions, use securenow_human_action_block. For rate-limit decisions, use securenow_rate_limit_create_from_text and then close the row with securenow_human_action_close_no_write outcome=rate_limited. For false positives, use securenow_human_action_false_positive with restrictive conditions. For case-level tune_rule/create_exclusion rows, inspect securenow_notifications_get and then use securenow_human_case_action_update: approve only after the change is applied, reject when the plan is vague or wrong.',
|
|
2555
2611
|
'End with counts: handled, blocked/proposed block, rate limits created/proposed, false positives, rule tuning/admin permission blockers with exact guard, partial resolutions, skipped with missing proof, and still waiting.',
|
|
2556
2612
|
].join('\n'),
|
|
2557
2613
|
},
|
|
@@ -2578,7 +2634,7 @@ function promptMessages(name, args = {}) {
|
|
|
2578
2634
|
'After a rule fix, close the covered rows with securenow_alert_review_run_apply_group_decision (use previewOnly:true first; set allowMixed only after reviewing every row). For correct detections use securenow_alert_review_run_keep_alive. For one-off rows use securenow_alert_review_run_resolve or securenow_alert_review_run_dismiss with a full decision report.',
|
|
2579
2635
|
confirmWrites
|
|
2580
2636
|
? 'The user requested execution. Call write tools with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant.'
|
|
2581
|
-
: 'Do not execute write tools yet. Produce a group-by-group plan with the exact validated guards and MCP write calls for user approval.',
|
|
2637
|
+
: 'Do not execute write tools yet. Read-only tools are fine, and so are the dry-run test tools (securenow_alert_rule_candidate_test, securenow_alert_rule_condition_candidate_test, securenow_alert_rule_condition_diff): they change nothing (pass confirm:true), so validate every guard you propose. They need an alerts:write key: on a read-only key, mark each guard untested instead. Produce a group-by-group plan with the exact validated guards and MCP write calls for user approval.',
|
|
2582
2638
|
'Never resolve a group as false positive without concrete benign samples AND proof the guard preserves attack detection. When uncertain, keep the rule alive and leave the row open with the missing proof named.',
|
|
2583
2639
|
'End with counts: groups worked, rules tuned (with the exact guards), rows resolved/dismissed/kept alive, rows left open with missing proof, and any permission blockers.',
|
|
2584
2640
|
].join('\n'),
|
|
@@ -2597,23 +2653,24 @@ function promptMessages(name, args = {}) {
|
|
|
2597
2653
|
content: {
|
|
2598
2654
|
type: 'text',
|
|
2599
2655
|
text: [
|
|
2600
|
-
'Run one scheduled SecureNow SOC patrol pass using the MCP tools.
|
|
2656
|
+
'Run one scheduled SecureNow SOC patrol pass using the MCP tools. It works the two review queues: Alert Review ("is this rule right?": noisy or self-involving alert runs) and Requires Human ("what do we do about this IP or plan?": decisions SecureNow prepared but will not make alone). Be conservative and auditable, and finish in one bounded pass.',
|
|
2601
2657
|
'',
|
|
2602
2658
|
'Phase 0 — check for work:',
|
|
2603
2659
|
'- Call securenow_alert_review_runs_count and securenow_human_actions_list (page=1, limit=1).',
|
|
2604
2660
|
'- If both queues are empty, stop and report "no open SOC work".',
|
|
2661
|
+
...(args.search ? [`- Scope: only work rows and groups matching "${args.search}" (pass it as the search argument to the list tools); leave everything else for another pass.`] : []),
|
|
2605
2662
|
'',
|
|
2606
2663
|
'Phase 1 — Alert Review first (root-cause rule fixes are the upstream fix; they auto-resolve downstream human work):',
|
|
2607
2664
|
`- Follow the work_alert_review runbook for up to ${maxAlertReviewGroups} root-cause group(s): list groups, inspect prototypes and siblings, validate narrow attack-preserving guards (two or more of path/method/status_code/user_agent) with condition tests/diffs before applying, tune rules, batch-resolve covered rows, keep real detections alive, and leave unclear rows open with the exact missing proof.`,
|
|
2608
2665
|
'',
|
|
2609
2666
|
'Phase 2 — Requires Human queue:',
|
|
2610
|
-
`- Follow the work_human_actions runbook for up to ${maxHumanRows} row(s): list the queue most urgent first, read each AI report and trace evidence, then choose exactly one outcome per row (block, rate limit, scoped false positive, rule tuning, case action approve/reject, or
|
|
2667
|
+
`- Follow the work_human_actions runbook for up to ${maxHumanRows} row(s): list the queue most urgent first, read each AI report and trace evidence, then choose exactly one outcome per row (block, rate limit, scoped false positive, rule tuning, case action approve/reject, or close without enforcement via securenow_human_action_close_no_write with the missing proof). A decision report alone does not close a row.`,
|
|
2611
2668
|
'- Re-check rows against any rule changes from Phase 1 before deciding — a Phase 1 fix often already covers them.',
|
|
2612
2669
|
'',
|
|
2613
2670
|
'Safety (this run may be unattended):',
|
|
2614
2671
|
confirmWrites
|
|
2615
2672
|
? '- Writes are allowed this pass: call write tools with confirm:true, a precise reason, and a full decisionReport. Never block without high-confidence malicious evidence (riskScore >= 90 or equivalent proof). Never create broad false positives or single-condition exclusions. Prefer an auditable no-write decision report whenever evidence is mixed.'
|
|
2616
|
-
: '- Read-only pass: do NOT call write tools. Produce the exact prepared write calls (with guards and reasons) as a reviewable plan.',
|
|
2673
|
+
: '- Read-only pass: do NOT call write tools, except the dry-run test tools (securenow_alert_rule_candidate_test, securenow_alert_rule_condition_candidate_test, securenow_alert_rule_condition_diff), which change nothing (on a read-only key they are refused: mark those guards untested). Produce the exact prepared write calls (with guards and reasons) as a reviewable plan.',
|
|
2617
2674
|
'- Never trust an IP globally, never create IP allowlist entries, never suppress on attacker-controllable fields alone.',
|
|
2618
2675
|
'- Stay within the row/group budgets; leave the rest for the next scheduled pass.',
|
|
2619
2676
|
'',
|
|
@@ -2704,8 +2761,40 @@ function mcpToolDefinition(tool) {
|
|
|
2704
2761
|
};
|
|
2705
2762
|
}
|
|
2706
2763
|
|
|
2707
|
-
|
|
2708
|
-
|
|
2764
|
+
// Tool profiles: a smaller tool list for an agent that does one job (fewer tools, less context).
|
|
2765
|
+
// Every profile keeps the auth and app tools. `securenow mcp --profile <name>` (or
|
|
2766
|
+
// SECURENOW_MCP_PROFILE) picks one; the default is every tool.
|
|
2767
|
+
const PROFILE_BASE_TOOLS = new Set(['securenow_auth_status', 'securenow_apps_list', 'securenow_apps_info']);
|
|
2768
|
+
const TOOL_PROFILES = {
|
|
2769
|
+
investigator: {
|
|
2770
|
+
description: 'Read-only investigation: notifications, traces, logs, IP intelligence, forensics questions and chat, rule run history and "why didn\'t this alert".',
|
|
2771
|
+
includes: (tool) => tool.readOnly || /^securenow_forensics_(query|chat|chat_confirm)$/.test(tool.name),
|
|
2772
|
+
},
|
|
2773
|
+
detection: {
|
|
2774
|
+
description: 'Write and tune detections: draft, create, test, run and explain alert rules, with the traces, logs, notifications and forensics to check them against.',
|
|
2775
|
+
includes: (tool) => /^securenow_alert_rules?_/.test(tool.name)
|
|
2776
|
+
|| /^securenow_(traces|logs|forensics|ip)_/.test(tool.name)
|
|
2777
|
+
|| /^securenow_notifications_(list|get|batch_get)$/.test(tool.name),
|
|
2778
|
+
},
|
|
2779
|
+
responder: {
|
|
2780
|
+
description: 'Respond to alerts: notifications, the human action queue and alert reviews, blocks, allowlist, trusted IPs, rate limits, challenges, automations, firewall and IP intelligence.',
|
|
2781
|
+
includes: (tool) => /^securenow_(notifications|human|alert_review|blocklist|allowlist|trusted|rate_limit|challenge|automation|ip|firewall)_/.test(tool.name)
|
|
2782
|
+
|| ['securenow_alert_rule_get', 'securenow_alert_rule_runs', 'securenow_alert_rule_explain', 'securenow_traces_list', 'securenow_traces_show', 'securenow_logs_for_trace'].includes(tool.name),
|
|
2783
|
+
},
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2786
|
+
function toolsForProfile(profile) {
|
|
2787
|
+
const name = String(profile || 'all').trim().toLowerCase();
|
|
2788
|
+
if (name === 'all') return TOOLS;
|
|
2789
|
+
const selected = TOOL_PROFILES[name];
|
|
2790
|
+
if (!selected) {
|
|
2791
|
+
throw new Error(`Unknown MCP profile "${profile}". Use one of: all, ${Object.keys(TOOL_PROFILES).join(', ')}`);
|
|
2792
|
+
}
|
|
2793
|
+
return TOOLS.filter((tool) => PROFILE_BASE_TOOLS.has(tool.name) || selected.includes(tool));
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
function listTools(profile) {
|
|
2797
|
+
return toolsForProfile(profile).map(mcpToolDefinition);
|
|
2709
2798
|
}
|
|
2710
2799
|
|
|
2711
2800
|
function listResources() {
|
|
@@ -2945,7 +3034,29 @@ function summarizeRateLimit(r) {
|
|
|
2945
3034
|
return { ...r, metadata: undefined, effectiveStatus: r.status === 'active' && expired ? 'expired' : r.status };
|
|
2946
3035
|
}
|
|
2947
3036
|
|
|
3037
|
+
// One trace's spans repeat the same resource fields (service, environment, SDK...) on every span:
|
|
3038
|
+
// keep those once under `shared` and drop empty fields, so a trace costs far less context.
|
|
3039
|
+
function compactSpans(res) {
|
|
3040
|
+
if (!Array.isArray(res?.spans) || res.spans.length < 2) return res;
|
|
3041
|
+
const spans = res.spans;
|
|
3042
|
+
const isEmpty = (v) => v == null || v === '' || (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0)
|
|
3043
|
+
|| (Array.isArray(v) && v.length === 0);
|
|
3044
|
+
const shared = {};
|
|
3045
|
+
for (const key of new Set(spans.flatMap((span) => Object.keys(span || {})))) {
|
|
3046
|
+
const first = JSON.stringify(spans[0]?.[key]);
|
|
3047
|
+
if (spans.every((span) => JSON.stringify(span?.[key]) === first)) shared[key] = spans[0]?.[key];
|
|
3048
|
+
}
|
|
3049
|
+
const keep = (span) => Object.fromEntries(Object.entries(span || {}).filter(([key, value]) => !(key in shared) && !isEmpty(value)));
|
|
3050
|
+
return {
|
|
3051
|
+
...res,
|
|
3052
|
+
_compact: 'Fields identical on every span are listed once under shared; empty fields are left out.',
|
|
3053
|
+
shared: Object.fromEntries(Object.entries(shared).filter(([, value]) => !isEmpty(value))),
|
|
3054
|
+
spans: spans.map(keep),
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2948
3058
|
const SUMMARIZERS = {
|
|
3059
|
+
securenow_traces_show: compactSpans,
|
|
2949
3060
|
securenow_notifications_list: (res) => (Array.isArray(res?.notifications) ? { ...res, notifications: res.notifications.map(summarizeNotification) } : res),
|
|
2950
3061
|
securenow_alert_rules_list: (res) => {
|
|
2951
3062
|
if (!Array.isArray(res?.alertRules)) return res;
|
|
@@ -3000,6 +3111,8 @@ module.exports = {
|
|
|
3000
3111
|
getTool,
|
|
3001
3112
|
mcpToolDefinition,
|
|
3002
3113
|
listTools,
|
|
3114
|
+
toolsForProfile,
|
|
3115
|
+
TOOL_PROFILES,
|
|
3003
3116
|
listResources,
|
|
3004
3117
|
listPrompts,
|
|
3005
3118
|
promptMessages,
|
package/mcp/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
RESOURCES,
|
|
10
10
|
getTool,
|
|
11
11
|
listTools,
|
|
12
|
+
toolsForProfile,
|
|
12
13
|
listResources,
|
|
13
14
|
listPrompts,
|
|
14
15
|
promptMessages,
|
|
@@ -26,6 +27,17 @@ const {
|
|
|
26
27
|
const SERVER_NAME = 'securenow';
|
|
27
28
|
const PROTOCOL_VERSION = '2025-06-18';
|
|
28
29
|
|
|
30
|
+
// `securenow mcp --profile investigator|detection|responder` (or SECURENOW_MCP_PROFILE) serves a
|
|
31
|
+
// subset of the tools; the default is every tool.
|
|
32
|
+
const PROFILE = String(process.env.SECURENOW_MCP_PROFILE || 'all').trim().toLowerCase() || 'all';
|
|
33
|
+
let PROFILE_TOOL_NAMES;
|
|
34
|
+
try {
|
|
35
|
+
PROFILE_TOOL_NAMES = new Set(toolsForProfile(PROFILE).map((tool) => tool.name));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
process.stderr.write(`[securenow mcp] ${err.message}\n`);
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
function write(message) {
|
|
30
42
|
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
31
43
|
}
|
|
@@ -201,7 +213,7 @@ function readResource(uri) {
|
|
|
201
213
|
|
|
202
214
|
const text = resourceContent(resource, {
|
|
203
215
|
projectConfig: () => jsonText(localAuthStatus()),
|
|
204
|
-
toolsCatalog: () => jsonText(listTools().map((tool) => ({
|
|
216
|
+
toolsCatalog: () => jsonText(listTools(PROFILE).map((tool) => ({
|
|
205
217
|
name: tool.name,
|
|
206
218
|
description: tool.description,
|
|
207
219
|
readOnly: tool.annotations?.readOnlyHint === true,
|
|
@@ -240,7 +252,7 @@ async function handleRequest(message) {
|
|
|
240
252
|
}
|
|
241
253
|
|
|
242
254
|
if (method === 'ping') return ok(id, {});
|
|
243
|
-
if (method === 'tools/list') return ok(id, { tools: listTools() });
|
|
255
|
+
if (method === 'tools/list') return ok(id, { tools: listTools(PROFILE) });
|
|
244
256
|
if (method === 'resources/list') return ok(id, { resources: listResources() });
|
|
245
257
|
if (method === 'prompts/list') return ok(id, { prompts: listPrompts() });
|
|
246
258
|
|
|
@@ -262,6 +274,9 @@ async function handleRequest(message) {
|
|
|
262
274
|
const args = params.arguments || {};
|
|
263
275
|
const tool = getTool(name);
|
|
264
276
|
if (!tool) throw new Error(`Unknown tool: ${name}`);
|
|
277
|
+
if (!PROFILE_TOOL_NAMES.has(name)) {
|
|
278
|
+
throw new Error(`Tool ${name} is not in the "${PROFILE}" MCP profile. Restart the server without --profile (or with another profile) to use it.`);
|
|
279
|
+
}
|
|
265
280
|
|
|
266
281
|
const result = await callApiTool(tool, args);
|
|
267
282
|
return ok(id, asToolResult({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.19.0",
|
|
4
4
|
"mcpName": "ai.securenow/securenow",
|
|
5
5
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"securenow-mcp": "mcp/server.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-firewall-paths.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-crash-capture.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-package-contents.js"
|
|
14
|
+
"test": "node test-app-config.js && node test-resolve-ip.js && node test-otel-defaults.js && node test-telemetry.js && node test-legacy-semconv.js && node test-otp-redaction.js && node test-header-redaction.js && node test-firewall-paths.js && node test-rate-limits.js && node test-rate-limit-redis.js && node test-log-format.js && node test-crash-capture.js && node test-sdk-disabled.js && node test-internal-thread.js && node test-esm-detect.js && node test-package-contents.js"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"opentelemetry",
|
|
@@ -160,6 +160,7 @@
|
|
|
160
160
|
"challenge.js",
|
|
161
161
|
"otel-defaults.js",
|
|
162
162
|
"internal-thread.js",
|
|
163
|
+
"esm-detect.js",
|
|
163
164
|
"legacy-semconv.js",
|
|
164
165
|
"otp-redaction.js",
|
|
165
166
|
"header-redaction.js",
|
package/register.js
CHANGED
|
@@ -14,13 +14,9 @@ if (require('./sdk-disabled').sdkDisabled('register')) return;
|
|
|
14
14
|
// 1. Auto-register the ESM loader hook so customers never need --import
|
|
15
15
|
(() => {
|
|
16
16
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(pkgPath)) return;
|
|
21
|
-
|
|
22
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
23
|
-
if (pkg.type !== 'module') return;
|
|
17
|
+
// ESM is decided by the entry script's package.json, not only the working directory's
|
|
18
|
+
// (process managers often start apps from another directory). See esm-detect.js.
|
|
19
|
+
if (!require('./esm-detect').detectEsmApp().esm) return;
|
|
24
20
|
|
|
25
21
|
// Already registered via --import?
|
|
26
22
|
const execArgv = process.execArgv.join(' ');
|
package/tracing.js
CHANGED
|
@@ -338,19 +338,13 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
338
338
|
// (e.g. Node 18, or require('securenow/tracing') called directly without register.js).
|
|
339
339
|
(() => {
|
|
340
340
|
try {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
349
|
-
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
350
|
-
if (!hasCliHook && !hasModuleRegister) {
|
|
351
|
-
console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
352
|
-
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
353
|
-
}
|
|
341
|
+
if (require('./esm-detect').detectEsmApp().esm) {
|
|
342
|
+
const execArgv = process.execArgv.join(' ');
|
|
343
|
+
const hasCliHook = execArgv.includes('hook.mjs') || execArgv.includes('import-in-the-middle');
|
|
344
|
+
const hasModuleRegister = typeof require('node:module').register === 'function';
|
|
345
|
+
if (!hasCliHook && !hasModuleRegister) {
|
|
346
|
+
console.warn('[securenow] ⚠️ ESM app detected ("type": "module") but no ESM loader hook available.');
|
|
347
|
+
console.warn('[securenow] Upgrade to Node >=20.6 (recommended) or add: --import @opentelemetry/instrumentation/hook.mjs');
|
|
354
348
|
}
|
|
355
349
|
}
|
|
356
350
|
} catch (_) {}
|