securenow 7.7.5 → 7.7.7
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 +7 -4
- package/README.md +2 -0
- package/SKILL-API.md +19 -3
- package/SKILL-CLI.md +27 -2
- package/cli/automation.js +29 -1
- package/cli/human.js +123 -7
- package/cli/security.js +34 -24
- package/cli.js +19 -7
- package/mcp/catalog.js +272 -18
- package/package.json +1 -1
package/NPM_README.md
CHANGED
|
@@ -186,7 +186,7 @@ codex mcp add securenow -- npx securenow mcp
|
|
|
186
186
|
npx -p securenow securenow-mcp
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason.
|
|
189
|
+
The MCP surface exposes tools for applications, traces, logs, firewall, IP intelligence, forensics, notifications, blocklist, allowlist, trusted IPs, and docs-backed prompts/resources. Write actions require `confirm:true` and a reason. Use `securenow_blocklist_unblock` to stop firewall enforcement while keeping the block report/history; `securenow_blocklist_remove` is a compatibility alias.
|
|
190
190
|
|
|
191
191
|
For hosted clients, SecureNow can expose the same surface at `https://api.securenow.ai/mcp`. The hosted endpoint uses the same API authentication and scope checks as the rest of SecureNow.
|
|
192
192
|
|
|
@@ -263,7 +263,7 @@ npx securenow alerts history --limit 20
|
|
|
263
263
|
### IP Intelligence & Blocklist
|
|
264
264
|
|
|
265
265
|
```bash
|
|
266
|
-
# Look up any IP -- geo,
|
|
266
|
+
# Look up any IP -- geo, SecureNow IPDB score, verdict, risk factors
|
|
267
267
|
npx securenow ip 203.0.113.42
|
|
268
268
|
|
|
269
269
|
# Show traces from a specific IP
|
|
@@ -272,7 +272,9 @@ npx securenow ip traces 203.0.113.42
|
|
|
272
272
|
# Manage the blocklist
|
|
273
273
|
npx securenow blocklist
|
|
274
274
|
npx securenow blocklist add 203.0.113.42 --reason "Brute force scanner"
|
|
275
|
-
npx securenow blocklist
|
|
275
|
+
npx securenow blocklist unblock <id> --reason "Reviewed as safe"
|
|
276
|
+
npx securenow blocklist remove <id> # compatibility alias
|
|
277
|
+
npx securenow blocklist list --status removed
|
|
276
278
|
npx securenow blocklist stats
|
|
277
279
|
|
|
278
280
|
# Manage trusted IPs
|
|
@@ -520,7 +522,8 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
520
522
|
| | `automation dry-run <id>` | Preview automation matches without writing blocks |
|
|
521
523
|
| | `automation execute <id> --yes` | Run an automation rule now |
|
|
522
524
|
| | `blocklist add <ip>` | Block IP |
|
|
523
|
-
| | `blocklist
|
|
525
|
+
| | `blocklist unblock <id>` | Unblock IP and retain report/history |
|
|
526
|
+
| | `blocklist remove <id>` | Compatibility alias for unblock |
|
|
524
527
|
| | `blocklist stats` | Block stats |
|
|
525
528
|
| | `allowlist` | Allowed IPs (restrict-mode) |
|
|
526
529
|
| | `allowlist add <ip>` | Allow IP (`--label`, `--reason`) |
|
package/README.md
CHANGED
|
@@ -191,6 +191,7 @@ npx securenow doctor # diagnose config + connectivity
|
|
|
191
191
|
# Security
|
|
192
192
|
npx securenow firewall status --env production
|
|
193
193
|
npx securenow blocklist add 1.2.3.4 --reason "scanner"
|
|
194
|
+
npx securenow blocklist unblock <id> --reason "reviewed safe"
|
|
194
195
|
npx securenow fp ai-fill --description "Stripe webhook POST /api/stripe/webhook"
|
|
195
196
|
|
|
196
197
|
# Telemetry from shell (no SDK boot)
|
|
@@ -357,6 +358,7 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
357
358
|
|---|---|
|
|
358
359
|
| `securenow blocklist` | List blocked IPs |
|
|
359
360
|
| `securenow blocklist add <ip> [--reason ...]` | Block an IP |
|
|
361
|
+
| `securenow blocklist unblock <id> [--reason ...]` | Stop enforcement and keep block history |
|
|
360
362
|
| `securenow allowlist add <ip>` | Allow an IP (restrict-mode) |
|
|
361
363
|
| `securenow trusted add <ip>` | Mark an IP as trusted |
|
|
362
364
|
|
package/SKILL-API.md
CHANGED
|
@@ -66,9 +66,25 @@ npx securenow api-key set snk_live_abc123...
|
|
|
66
66
|
|
|
67
67
|
Both paths write the key to `.securenow/credentials.json` (auto-gitignored) and the firewall activates on next start. For production, run `npx securenow credentials runtime --env production` and mount/copy the tokenless file as `.securenow/credentials.json`.
|
|
68
68
|
|
|
69
|
-
The firewall syncs your blocklist and enforces it on every request — zero code changes.
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
The firewall syncs your blocklist and enforces it on every request — zero code changes.
|
|
70
|
+
|
|
71
|
+
Blocklist unblocks are audit-preserving: dashboard/API/CLI/MCP unblock actions
|
|
72
|
+
mark the active block as `removed`, invalidate firewall sync, clear expiry to
|
|
73
|
+
avoid TTL deletion, and retain block reports/history for future review or
|
|
74
|
+
reblock context.
|
|
75
|
+
|
|
76
|
+
For near-realtime propagation after a block/unblock, set
|
|
77
|
+
`SECURENOW_FIREWALL_VERSION_INTERVAL=1` or `2` in the protected app. The SDK
|
|
78
|
+
polls `/firewall/sync` with ETag/304, so unchanged checks are lightweight; keep
|
|
79
|
+
`SECURENOW_FIREWALL_SYNC_INTERVAL` high as a safety-net full refresh.
|
|
80
|
+
|
|
81
|
+
Default automation is active for new and existing customers. The API
|
|
82
|
+
idempotently provisions risk-score rules for all apps/environments:
|
|
83
|
+
`riskScore >= 95` blocks for 7 days, `90-94` for 72h, and `85-89` for 24h.
|
|
84
|
+
Run `securenow automation defaults --yes` or the API backfill script when an
|
|
85
|
+
operator needs to ensure those defaults immediately.
|
|
86
|
+
|
|
87
|
+
---
|
|
72
88
|
|
|
73
89
|
## Import Map
|
|
74
90
|
|
package/SKILL-CLI.md
CHANGED
|
@@ -221,7 +221,7 @@ Use this for the same work shown in **Requires Human**: AI has already grouped a
|
|
|
221
221
|
```bash
|
|
222
222
|
securenow human # list human decisions, most urgent first
|
|
223
223
|
securenow human list --limit 20
|
|
224
|
-
securenow human show 1 # inspect row 1 with AI report,
|
|
224
|
+
securenow human show 1 # inspect row 1 with AI report, investigation steps, proofs, trace links
|
|
225
225
|
securenow human block 1 --yes --reason "AI evidence confirmed malicious"
|
|
226
226
|
securenow human fp 1 --yes --reason "Scoped false positive after evidence review"
|
|
227
227
|
securenow human action 1 --status rejected --yes --reason "Tuning guard is too broad"
|
|
@@ -300,18 +300,43 @@ securenow firewall test-ip <ip> --app <key> --env local # check if IP would be
|
|
|
300
300
|
securenow blocklist # list blocked IPs
|
|
301
301
|
securenow blocklist list
|
|
302
302
|
securenow blocklist add <ip> --app <key> --env production --reason "Brute force"
|
|
303
|
-
securenow blocklist
|
|
303
|
+
securenow blocklist unblock <id> --reason "False alarm after review"
|
|
304
|
+
securenow blocklist remove <id> # compatibility alias for unblock
|
|
305
|
+
securenow blocklist list --status removed # audit retained unblocks
|
|
304
306
|
securenow blocklist stats # block counts, top reasons
|
|
305
307
|
```
|
|
306
308
|
|
|
309
|
+
Unblock stops firewall enforcement but preserves the block report, history, and
|
|
310
|
+
unblock audit fields. Reblocking the same IP later creates a fresh active block
|
|
311
|
+
without erasing the previous investigation trail.
|
|
312
|
+
|
|
313
|
+
MCP exposes legacy pending-block cleanup separately from current Requires Human
|
|
314
|
+
work:
|
|
315
|
+
|
|
316
|
+
- `securenow_blocklist_unblock`
|
|
317
|
+
- `securenow_blocklist_remove` (compatibility alias for unblock)
|
|
318
|
+
- `securenow_blocklist_pending_list`
|
|
319
|
+
- `securenow_blocklist_pending_approve`
|
|
320
|
+
- `securenow_blocklist_pending_reject`
|
|
321
|
+
- `securenow_blocklist_pending_bulk_approve`
|
|
322
|
+
- `securenow_blocklist_pending_bulk_reject`
|
|
323
|
+
- prompt: `cleanup_legacy_pending_blocks`
|
|
324
|
+
|
|
307
325
|
### Automation Rules
|
|
308
326
|
|
|
309
327
|
```bash
|
|
310
328
|
securenow automation # list blocklist automation rules
|
|
329
|
+
securenow automation defaults --yes # ensure built-in default risk-score rules
|
|
311
330
|
securenow automation show <id>
|
|
312
331
|
securenow automation dry-run <id> --limit 500
|
|
313
332
|
securenow automation execute <id> --yes
|
|
314
333
|
```
|
|
334
|
+
|
|
335
|
+
Built-in default automation is enabled by default for all apps/environments and
|
|
336
|
+
uses the canonical `riskScore`: 95-100 blocks for 7 days, 90-94 blocks for 72h,
|
|
337
|
+
85-89 blocks for 24h, and scores below 85 stay in investigation/review unless
|
|
338
|
+
the customer adds a stricter custom rule. Raw SecureNow IPDB confidence remains
|
|
339
|
+
supporting evidence, not the primary automation score.
|
|
315
340
|
|
|
316
341
|
### Allowlist — Restrict to Known IPs
|
|
317
342
|
|
package/cli/automation.js
CHANGED
|
@@ -249,6 +249,34 @@ async function execute(args, flags) {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
async function defaults(args, flags) {
|
|
253
|
+
requireAuth();
|
|
254
|
+
if ((flags['force-enable'] || flags.force) && !flags.yes) {
|
|
255
|
+
const ok = await ui.confirm('Re-enable existing SecureNow default automation rules that were disabled?');
|
|
256
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const s = ui.spinner('Ensuring default automation rules');
|
|
260
|
+
try {
|
|
261
|
+
const data = await api.post('/automation-rules/defaults/ensure', {
|
|
262
|
+
forceEnableExisting: !!(flags['force-enable'] || flags.force),
|
|
263
|
+
});
|
|
264
|
+
s.stop('Default automation rules ensured');
|
|
265
|
+
if (flags.json) { ui.json(data); return; }
|
|
266
|
+
|
|
267
|
+
const result = data.result || {};
|
|
268
|
+
ui.keyValue([
|
|
269
|
+
['Version', String(result.version ?? '-')],
|
|
270
|
+
['Created', String(result.created ?? 0)],
|
|
271
|
+
['Updated', String(result.updated ?? 0)],
|
|
272
|
+
['Already present', String(result.alreadyProvisioned ?? 0)],
|
|
273
|
+
]);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
s.fail('Failed to ensure default automation rules');
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
252
280
|
async function remove(args, flags) {
|
|
253
281
|
requireAuth();
|
|
254
282
|
const id = args[0];
|
|
@@ -272,4 +300,4 @@ async function remove(args, flags) {
|
|
|
272
300
|
}
|
|
273
301
|
}
|
|
274
302
|
|
|
275
|
-
module.exports = { list, show, create, update, dryRun, execute, remove };
|
|
303
|
+
module.exports = { list, show, create, update, dryRun, execute, defaults, remove };
|
package/cli/human.js
CHANGED
|
@@ -42,6 +42,78 @@ function parseTaskRef(ref) {
|
|
|
42
42
|
return { kind: 'notification', notificationId: text };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function parseJsonOrList(value) {
|
|
46
|
+
if (value == null || value === true) return [];
|
|
47
|
+
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
|
48
|
+
const text = String(value).trim();
|
|
49
|
+
if (!text) return [];
|
|
50
|
+
if (text.startsWith('[')) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(text);
|
|
53
|
+
return Array.isArray(parsed) ? parsed.filter(Boolean).map(String) : [];
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
return text.split(/\r?\n|;;/).map((item) => item.trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseCommaList(value) {
|
|
60
|
+
if (value == null || value === true) return [];
|
|
61
|
+
if (Array.isArray(value)) return value.flatMap(parseCommaList);
|
|
62
|
+
const text = String(value).trim();
|
|
63
|
+
if (!text) return [];
|
|
64
|
+
if (text.startsWith('[')) return parseJsonOrList(text);
|
|
65
|
+
return text.split(',').map((item) => item.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function defaultReviewedHistory(task = {}) {
|
|
69
|
+
const history = [];
|
|
70
|
+
if (taskLabel(task)) history.push(`Case: ${taskLabel(task)}`);
|
|
71
|
+
if (task.aiDecision?.label || task.aiDecision?.recommendation) history.push(`AI recommendation: ${task.aiDecision.label || task.aiDecision.recommendation}`);
|
|
72
|
+
if (task.aiDecision?.reason) history.push(`AI reason: ${task.aiDecision.reason}`);
|
|
73
|
+
if (task.aiDecision?.confidence != null) history.push(`AI confidence: ${Math.round(Number(task.aiDecision.confidence))}%`);
|
|
74
|
+
if (task.proofs?.paths?.length) history.push(`Paths: ${formatList(task.proofs.paths, 8)}`);
|
|
75
|
+
if (task.proofs?.methods?.length) history.push(`Methods: ${formatList(task.proofs.methods, 8)}`);
|
|
76
|
+
if (task.proofs?.statusCodes?.length) history.push(`Status codes: ${formatList(task.proofs.statusCodes.map(String), 8)}`);
|
|
77
|
+
if (task.proofs?.userAgents?.length) history.push(`User agents: ${formatList(task.proofs.userAgents, 4)}`);
|
|
78
|
+
if (task.proofs?.traceIds?.length) history.push(`Trace IDs: ${formatList(task.proofs.traceIds, 8)}`);
|
|
79
|
+
return history;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildDecisionReport(task, flags = {}, outcome, fallbackSummary) {
|
|
83
|
+
let report = {};
|
|
84
|
+
if (flags.report) {
|
|
85
|
+
try {
|
|
86
|
+
report = JSON.parse(flags.report);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
ui.error(`Invalid --report JSON: ${err.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const explicitEvidence = parseJsonOrList(flags.evidence);
|
|
94
|
+
const explicitHistory = parseJsonOrList(flags.history);
|
|
95
|
+
const explicitMissingProof = parseJsonOrList(flags['missing-proof']);
|
|
96
|
+
const explicitRecommendations = parseJsonOrList(flags.recommendations);
|
|
97
|
+
const traceIds = parseCommaList(flags['trace-ids']);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
source: 'cli',
|
|
101
|
+
outcome: flags.outcome || report.outcome || outcome || 'other',
|
|
102
|
+
summary: flags.summary || report.summary || fallbackSummary || flags.reason || '',
|
|
103
|
+
reason: flags.reason || report.reason || '',
|
|
104
|
+
evidence: explicitEvidence.length ? explicitEvidence : asArray(report.evidence),
|
|
105
|
+
reviewedHistory: explicitHistory.length ? explicitHistory : (asArray(report.reviewedHistory).length ? asArray(report.reviewedHistory) : defaultReviewedHistory(task)),
|
|
106
|
+
traceIds: traceIds.length ? traceIds : (asArray(report.traceIds).length ? asArray(report.traceIds) : asArray(task?.proofs?.traceIds)),
|
|
107
|
+
paths: asArray(report.paths).length ? asArray(report.paths) : asArray(task?.proofs?.paths),
|
|
108
|
+
methods: asArray(report.methods).length ? asArray(report.methods) : asArray(task?.proofs?.methods),
|
|
109
|
+
statusCodes: asArray(report.statusCodes).length ? asArray(report.statusCodes) : asArray(task?.proofs?.statusCodes).map(String),
|
|
110
|
+
userAgents: asArray(report.userAgents).length ? asArray(report.userAgents) : asArray(task?.proofs?.userAgents),
|
|
111
|
+
missingProof: explicitMissingProof.length ? explicitMissingProof : asArray(report.missingProof),
|
|
112
|
+
recommendations: explicitRecommendations.length ? explicitRecommendations : asArray(report.recommendations),
|
|
113
|
+
rawData: report.rawData || null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
45
117
|
async function fetchQueue(flags = {}) {
|
|
46
118
|
const query = {
|
|
47
119
|
page: flags.page || 1,
|
|
@@ -180,7 +252,7 @@ function printAiReport(ipReport = {}, notification = {}) {
|
|
|
180
252
|
|
|
181
253
|
if (steps.length) {
|
|
182
254
|
console.log('');
|
|
183
|
-
console.log(` ${ui.c.bold('
|
|
255
|
+
console.log(` ${ui.c.bold('Investigation steps')}`);
|
|
184
256
|
const rows = steps.map((step) => [
|
|
185
257
|
step.id,
|
|
186
258
|
step.status || '-',
|
|
@@ -323,12 +395,13 @@ async function action(args, flags) {
|
|
|
323
395
|
|
|
324
396
|
const result = flags.result ? JSON.parse(flags.result) : {};
|
|
325
397
|
if (flags.reason) result.reason = flags.reason;
|
|
398
|
+
const decisionReport = buildDecisionReport(task || { notificationId, actionKey }, flags, flags.outcome || 'case_action', result.summary || result.reason || `Case action ${status}`);
|
|
326
399
|
|
|
327
400
|
const s = ui.spinner('Updating case action');
|
|
328
401
|
try {
|
|
329
402
|
const data = await api.put(
|
|
330
403
|
`/notifications/${encodeURIComponent(notificationId)}/agent-case/actions/${encodeURIComponent(actionKey)}`,
|
|
331
|
-
{ status, result }
|
|
404
|
+
{ status, result, reportSource: 'cli', ips: parseCommaList(flags.ips), decisionReport }
|
|
332
405
|
);
|
|
333
406
|
s.stop('Case action updated');
|
|
334
407
|
if (flags.json) { ui.json(data); return; }
|
|
@@ -358,6 +431,8 @@ async function block(args, flags) {
|
|
|
358
431
|
riskScore: task.aiDecision?.riskScore ?? undefined,
|
|
359
432
|
decisionSource: 'ai_dag',
|
|
360
433
|
note: reason,
|
|
434
|
+
reportSource: 'cli',
|
|
435
|
+
decisionReport: buildDecisionReport(task, flags, 'blocked', reason),
|
|
361
436
|
};
|
|
362
437
|
const data = await api.put(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/status`, body);
|
|
363
438
|
s.stop(`${task.ip} blocked`);
|
|
@@ -391,6 +466,8 @@ async function fp(args, flags) {
|
|
|
391
466
|
applyToExisting: flags['apply-existing'] != null ? flags['apply-existing'] !== 'false' : Boolean(scope.applyToExisting),
|
|
392
467
|
ruleScope: flags['rule-scope'] || scope.ruleScope || 'this_rule',
|
|
393
468
|
aiConfidence: task.aiDecision?.confidence ?? null,
|
|
469
|
+
reportSource: 'cli',
|
|
470
|
+
decisionReport: buildDecisionReport(task, flags, 'false_positive', reason),
|
|
394
471
|
};
|
|
395
472
|
const data = await api.post(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/false-positive`, body);
|
|
396
473
|
s.stop(`${task.ip} marked false positive`);
|
|
@@ -402,6 +479,43 @@ async function fp(args, flags) {
|
|
|
402
479
|
}
|
|
403
480
|
}
|
|
404
481
|
|
|
482
|
+
async function report(args, flags) {
|
|
483
|
+
requireAuth();
|
|
484
|
+
const ref = args[0];
|
|
485
|
+
const { task } = await resolveTask(ref, flags);
|
|
486
|
+
const outcome = flags.outcome || (task.kind === 'case_action' || task.actionKey ? 'case_action' : 'other');
|
|
487
|
+
const summary = flags.summary || flags.reason || `Decision report recorded for ${taskLabel(task)}`;
|
|
488
|
+
|
|
489
|
+
if (!flags.yes && !flags.force) {
|
|
490
|
+
const target = task.ip ? `${task.ip}` : `${task.notificationId}`;
|
|
491
|
+
const ok = await ui.confirm(`Record decision report for ${target}?`);
|
|
492
|
+
if (!ok) return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const s = ui.spinner('Recording decision report');
|
|
496
|
+
try {
|
|
497
|
+
const decisionReport = buildDecisionReport(task, flags, outcome, summary);
|
|
498
|
+
let data;
|
|
499
|
+
if (task.ip) {
|
|
500
|
+
data = await api.post(
|
|
501
|
+
`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/decision-report`,
|
|
502
|
+
{ reportSource: 'cli', reason: flags.reason || summary, outcome, decisionReport }
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
data = await api.post(
|
|
506
|
+
`/notifications/${encodeURIComponent(task.notificationId)}/decision-report`,
|
|
507
|
+
{ reportSource: 'cli', reason: flags.reason || summary, outcome, ips: parseCommaList(flags.ips), decisionReport }
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
s.stop('Decision report recorded');
|
|
511
|
+
if (flags.json) ui.json(data);
|
|
512
|
+
else ui.success(`Recorded decision report: ${decisionReport.outcome}`);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
s.fail('Failed to record decision report');
|
|
515
|
+
throw err;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
405
519
|
function mcpPromptText(ref, flags = {}) {
|
|
406
520
|
const rowText = ref ? `Start with human action row/id: ${ref}.` : 'Work the human action queue from most urgent to least urgent.';
|
|
407
521
|
const limit = flags.limit || 10;
|
|
@@ -417,14 +531,15 @@ function mcpPromptText(ref, flags = {}) {
|
|
|
417
531
|
ref
|
|
418
532
|
? '2. Select the requested row number or task id, then call securenow_notifications_get and securenow_human_action_report for its notificationId and IP.'
|
|
419
533
|
: '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
|
|
420
|
-
'3. Read the AI report, finalDecision,
|
|
534
|
+
'3. Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
421
535
|
'4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
|
|
422
536
|
'5. Decide one outcome: block the IP, mark a scoped false positive, recommend alert-rule tuning, or skip if evidence is ambiguous.',
|
|
423
|
-
'6. For block decisions, call securenow_human_action_block with confirm:true and a
|
|
424
|
-
'7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions,
|
|
537
|
+
'6. For block decisions, call securenow_human_action_block with confirm:true, a precise reason, and a decisionReport with summary/evidence/reviewedHistory/traceIds.',
|
|
538
|
+
'7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, a precise reason, and a decisionReport.',
|
|
425
539
|
'8. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
|
|
426
|
-
'9.
|
|
427
|
-
'10.
|
|
540
|
+
'9. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing status, call securenow_human_action_decision_report_add or securenow_human_case_decision_report_add with the missing proof.',
|
|
541
|
+
'10. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
542
|
+
'11. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
|
|
428
543
|
'',
|
|
429
544
|
'Safety:',
|
|
430
545
|
'- Do not call write tools without confirm:true and a reason.',
|
|
@@ -455,6 +570,7 @@ module.exports = {
|
|
|
455
570
|
action,
|
|
456
571
|
block,
|
|
457
572
|
fp,
|
|
573
|
+
report,
|
|
458
574
|
prompt,
|
|
459
575
|
work,
|
|
460
576
|
};
|
package/cli/security.js
CHANGED
|
@@ -339,6 +339,12 @@ async function blocklistList(args, flags) {
|
|
|
339
339
|
if (flags.limit) query.limit = flags.limit;
|
|
340
340
|
if (flags.app) query.appKey = flags.app;
|
|
341
341
|
if (flags.env || flags.environment) query.environment = flags.env || flags.environment;
|
|
342
|
+
if (flags.status) query.status = flags.status;
|
|
343
|
+
if (flags.search) query.search = flags.search;
|
|
344
|
+
if (flags.view) query.view = flags.view;
|
|
345
|
+
if (flags.approvalStatus || flags['approval-status']) {
|
|
346
|
+
query.approvalStatus = flags.approvalStatus || flags['approval-status'];
|
|
347
|
+
}
|
|
342
348
|
const data = await api.get('/blocklist', { query });
|
|
343
349
|
const items = data.blockedIps || [];
|
|
344
350
|
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
@@ -348,15 +354,17 @@ async function blocklistList(args, flags) {
|
|
|
348
354
|
console.log('');
|
|
349
355
|
const rows = items.map(b => [
|
|
350
356
|
ui.c.dim(ui.truncate(b._id, 12)),
|
|
351
|
-
ui.c.red(b.ip || b.cidr || '—'),
|
|
352
|
-
ui.truncate(b.reason || '', 40),
|
|
357
|
+
ui.c.red(b.ip || b.cidr || '—'),
|
|
358
|
+
ui.truncate(b.reason || '', 40),
|
|
353
359
|
b.source || '—',
|
|
360
|
+
b.status || 'active',
|
|
354
361
|
b.applicationKey || ui.c.dim('all apps'),
|
|
355
362
|
b.environment || ui.c.dim('all envs'),
|
|
356
363
|
ui.timeAgo(b.createdAt),
|
|
364
|
+
b.unblockedAt ? ui.timeAgo(b.unblockedAt) : ui.c.dim('—'),
|
|
357
365
|
b.expiresAt ? new Date(b.expiresAt).toLocaleDateString() : ui.c.dim('permanent'),
|
|
358
366
|
]);
|
|
359
|
-
ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'App', 'Env', 'Added', 'Expires'], rows);
|
|
367
|
+
ui.table(['ID', 'IP/CIDR', 'Reason', 'Source', 'Status', 'App', 'Env', 'Added', 'Unblocked', 'Expires'], rows);
|
|
360
368
|
console.log('');
|
|
361
369
|
} catch (err) {
|
|
362
370
|
s.fail('Failed to fetch blocklist');
|
|
@@ -390,26 +398,28 @@ async function blocklistAdd(args, flags) {
|
|
|
390
398
|
|
|
391
399
|
async function blocklistRemove(args, flags) {
|
|
392
400
|
requireAuth();
|
|
393
|
-
const id = args[0];
|
|
394
|
-
if (!id) {
|
|
395
|
-
ui.error('Blocklist entry ID required. Usage: securenow blocklist
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!flags.force && !flags.yes) {
|
|
400
|
-
const ok = await ui.confirm('
|
|
401
|
-
if (!ok) { ui.info('Cancelled'); return; }
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const s = ui.spinner('
|
|
405
|
-
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
s.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
401
|
+
const id = args[0];
|
|
402
|
+
if (!id) {
|
|
403
|
+
ui.error('Blocklist entry ID required. Usage: securenow blocklist unblock <id> [--reason <reason>]');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!flags.force && !flags.yes) {
|
|
408
|
+
const ok = await ui.confirm('Unblock this IP? Firewall enforcement stops, but block report and history stay saved.');
|
|
409
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const s = ui.spinner('Unblocking IP');
|
|
413
|
+
try {
|
|
414
|
+
const body = {};
|
|
415
|
+
if (flags.reason) body.reason = flags.reason;
|
|
416
|
+
await api.post(`/blocklist/${id}/unblock`, body);
|
|
417
|
+
s.stop('Unblocked; block report and history retained');
|
|
418
|
+
} catch (err) {
|
|
419
|
+
s.fail('Failed to unblock IP');
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
413
423
|
|
|
414
424
|
async function blocklistStats(args, flags) {
|
|
415
425
|
requireAuth();
|
|
@@ -926,7 +936,7 @@ async function ipLookup(args, flags) {
|
|
|
926
936
|
if (data.domain) pairs.push(['Domain', data.domain]);
|
|
927
937
|
if (data.isp) pairs.push(['ISP', data.isp]);
|
|
928
938
|
if (data.usageType) pairs.push(['Usage Type', data.usageType]);
|
|
929
|
-
if (data.abuseConfidenceScore != null) pairs.push(['
|
|
939
|
+
if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
|
|
930
940
|
if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
|
|
931
941
|
if (data.verdict) pairs.push(['Verdict', data.verdict]);
|
|
932
942
|
if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
|
package/cli.js
CHANGED
|
@@ -164,13 +164,22 @@ const COMMANDS = {
|
|
|
164
164
|
},
|
|
165
165
|
human: {
|
|
166
166
|
desc: 'Work the human action queue prepared by SecureNow AI',
|
|
167
|
-
usage: 'securenow human <list|show|block|fp|action|prompt|work> [row|notificationId:ip] [options]',
|
|
167
|
+
usage: 'securenow human <list|show|block|fp|report|action|prompt|work> [row|notificationId:ip] [options]',
|
|
168
168
|
flags: {
|
|
169
169
|
json: 'Output as JSON',
|
|
170
170
|
page: 'Queue page number',
|
|
171
171
|
limit: 'Queue page size',
|
|
172
172
|
search: 'Search IP, rule, path, or verdict',
|
|
173
173
|
reason: 'Reason for block/false-positive decisions',
|
|
174
|
+
summary: 'Decision report summary to write to the IP history',
|
|
175
|
+
outcome: 'Decision report outcome: blocked, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, other',
|
|
176
|
+
evidence: 'Decision evidence; repeat as newline/;; separated text or JSON array',
|
|
177
|
+
history: 'Reviewed history/proofs; newline/;; separated text or JSON array',
|
|
178
|
+
'trace-ids': 'Comma-separated trace IDs reviewed for the decision',
|
|
179
|
+
ips: 'Comma-separated IPs affected by a case-level decision report',
|
|
180
|
+
'missing-proof': 'Missing proof for skipped or ambiguous rows',
|
|
181
|
+
recommendations: 'Follow-up recommendations; newline/;; separated text or JSON array',
|
|
182
|
+
report: 'Full decision report JSON object',
|
|
174
183
|
yes: 'Confirm write actions without prompting',
|
|
175
184
|
force: 'Alias for --yes',
|
|
176
185
|
conditions: 'False-positive conditions JSON array',
|
|
@@ -185,9 +194,10 @@ const COMMANDS = {
|
|
|
185
194
|
},
|
|
186
195
|
sub: {
|
|
187
196
|
list: { desc: 'List human decisions AI prepared', run: (a, f) => require('./cli/human').list(a, f) },
|
|
188
|
-
show: { desc: 'Show one row with AI report, proofs,
|
|
197
|
+
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) },
|
|
189
198
|
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) },
|
|
190
199
|
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) },
|
|
200
|
+
report: { desc: 'Record a structured decision report on a row without changing status', usage: 'securenow human report <row|notificationId:ip> --yes --outcome ambiguous --summary "..."', run: (a, f) => require('./cli/human').report(a, f) },
|
|
191
201
|
action: { desc: 'Approve/reject/execute a case-level proposed action', usage: 'securenow human action <row|notificationId> [actionKey] --status approved --yes --reason "..."', run: (a, f) => require('./cli/human').action(a, f) },
|
|
192
202
|
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) },
|
|
193
203
|
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) },
|
|
@@ -245,7 +255,7 @@ const COMMANDS = {
|
|
|
245
255
|
},
|
|
246
256
|
automation: {
|
|
247
257
|
desc: 'Manage automation rules for blocklist actions',
|
|
248
|
-
usage: 'securenow automation <list|show|create|update|dry-run|execute|delete> [rule-id] [options]',
|
|
258
|
+
usage: 'securenow automation <list|defaults|show|create|update|dry-run|execute|delete> [rule-id] [options]',
|
|
249
259
|
flags: {
|
|
250
260
|
json: 'Output as JSON',
|
|
251
261
|
body: 'Full JSON body for create/update',
|
|
@@ -270,6 +280,7 @@ const COMMANDS = {
|
|
|
270
280
|
},
|
|
271
281
|
sub: {
|
|
272
282
|
list: { desc: 'List automation rules', run: (a, f) => require('./cli/automation').list(a, f) },
|
|
283
|
+
defaults: { desc: 'Ensure SecureNow default risk-score automation rules', usage: 'securenow automation defaults [--force-enable] [--yes]', run: (a, f) => require('./cli/automation').defaults(a, f) },
|
|
273
284
|
show: { desc: 'Show automation rule details', usage: 'securenow automation show <rule-id>', run: (a, f) => require('./cli/automation').show(a, f) },
|
|
274
285
|
create: { desc: 'Create automation rule', usage: 'securenow automation create --name "..." --conditions \'[...]\' --actions \'[...]\'', run: (a, f) => require('./cli/automation').create(a, f) },
|
|
275
286
|
update: { desc: 'Update automation rule', usage: 'securenow automation update <rule-id> --body \'{...}\'', run: (a, f) => require('./cli/automation').update(a, f) },
|
|
@@ -282,13 +293,14 @@ const COMMANDS = {
|
|
|
282
293
|
blocklist: {
|
|
283
294
|
desc: 'Manage IP blocklist',
|
|
284
295
|
usage: 'securenow blocklist <subcommand> [options]',
|
|
285
|
-
flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', json: 'Output as JSON' },
|
|
296
|
+
flags: { app: 'Scope to app key', env: 'Scope to environment', environment: 'Alias for --env', page: 'Page number', limit: 'Max results', status: 'Entry status: active or removed', 'approval-status': 'Approval filter: pending, approved, or rejected', search: 'IP prefix search', view: 'List view: all or operational', reason: 'Audit reason for unblock', json: 'Output as JSON' },
|
|
286
297
|
sub: {
|
|
287
298
|
list: { desc: 'List blocked IPs', run: (a, f) => require('./cli/security').blocklistList(a, f) },
|
|
288
299
|
add: { desc: 'Block an IP', usage: 'securenow blocklist add <ip> [--app <key>] [--env production] [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistAdd(a, f) },
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
300
|
+
unblock: { desc: 'Unblock an IP and keep block history', usage: 'securenow blocklist unblock <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
|
|
301
|
+
remove: { desc: 'Unblock an IP (compatibility alias)', usage: 'securenow blocklist remove <id> [--reason <reason>]', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
|
|
302
|
+
stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
|
|
303
|
+
},
|
|
292
304
|
defaultSub: 'list',
|
|
293
305
|
},
|
|
294
306
|
allowlist: {
|
package/mcp/catalog.js
CHANGED
|
@@ -162,6 +162,26 @@ const environmentInput = {
|
|
|
162
162
|
environment: string('Deployment environment scope: production, staging, preview, local, test, or all. Default for investigations is production.'),
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
+
const decisionReportInput = {
|
|
166
|
+
decisionReport: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: true,
|
|
169
|
+
description: 'Structured audit report explaining the decision, evidence reviewed, trace IDs, missing proof, and recommendations.',
|
|
170
|
+
},
|
|
171
|
+
decisionSummary: string('Short decision summary to record on the IP/case history.'),
|
|
172
|
+
outcome: string('Decision outcome: blocked, false_positive, clean, deferred, case_action, rule_tuning, new_alert_rule, ambiguous, skipped, or other.'),
|
|
173
|
+
evidence: arrayOfStrings('Evidence strings that support the decision.'),
|
|
174
|
+
reviewedHistory: arrayOfStrings('History/proofs reviewed before deciding.'),
|
|
175
|
+
traceIds: arrayOfStrings('Trace IDs reviewed for this decision.'),
|
|
176
|
+
paths: arrayOfStrings('Paths/endpoints reviewed for this decision.'),
|
|
177
|
+
ips: arrayOfStrings('IP addresses affected by this case-level decision report.'),
|
|
178
|
+
methods: arrayOfStrings('HTTP methods reviewed for this decision.'),
|
|
179
|
+
statusCodes: arrayOfStrings('HTTP status codes reviewed for this decision.'),
|
|
180
|
+
userAgents: arrayOfStrings('User agents reviewed for this decision.'),
|
|
181
|
+
missingProof: arrayOfStrings('Proof that was missing when the row is skipped or ambiguous.'),
|
|
182
|
+
recommendations: arrayOfStrings('Follow-up recommendations to record with the decision.'),
|
|
183
|
+
};
|
|
184
|
+
|
|
165
185
|
const TOOLS = [
|
|
166
186
|
{
|
|
167
187
|
name: 'securenow_auth_status',
|
|
@@ -286,7 +306,7 @@ const TOOLS = [
|
|
|
286
306
|
bodyFields: ['confidenceMinimum', 'environment'],
|
|
287
307
|
inputSchema: objectSchema({
|
|
288
308
|
appKey: string('Application key UUID.'),
|
|
289
|
-
confidenceMinimum: number('Minimum SecureNow IPDB
|
|
309
|
+
confidenceMinimum: number('Minimum SecureNow IPDB confidence score.', { minimum: 0, maximum: 100 }),
|
|
290
310
|
...environmentInput,
|
|
291
311
|
...confirmSchema,
|
|
292
312
|
}, ['appKey', 'confidenceMinimum', 'confirm', 'reason']),
|
|
@@ -455,7 +475,7 @@ const TOOLS = [
|
|
|
455
475
|
{
|
|
456
476
|
name: 'securenow_human_action_report',
|
|
457
477
|
title: 'Get Human Action Report',
|
|
458
|
-
description: 'Fetch the full IP investigation report,
|
|
478
|
+
description: 'Fetch the full IP investigation report, investigation steps, proofs, metadata, and AI decision for one human action row.',
|
|
459
479
|
scope: 'notifications:read',
|
|
460
480
|
readOnly: true,
|
|
461
481
|
method: 'GET',
|
|
@@ -469,7 +489,7 @@ const TOOLS = [
|
|
|
469
489
|
{
|
|
470
490
|
name: 'securenow_human_action_block',
|
|
471
491
|
title: 'Approve AI Block Recommendation',
|
|
472
|
-
description: 'Approve the AI-prepared block decision for an IP. Write action; requires confirmation.',
|
|
492
|
+
description: 'Approve the AI-prepared block decision for an IP and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
|
|
473
493
|
scope: 'notifications:write',
|
|
474
494
|
readOnly: false,
|
|
475
495
|
destructive: true,
|
|
@@ -477,29 +497,50 @@ const TOOLS = [
|
|
|
477
497
|
method: 'PUT',
|
|
478
498
|
endpoint: '/notifications/:notificationId/ips/:ip/status',
|
|
479
499
|
pathParams: ['notificationId', 'ip'],
|
|
480
|
-
fixedBody: { status: 'blocked', decisionSource: 'ai_dag' },
|
|
500
|
+
fixedBody: { status: 'blocked', decisionSource: 'ai_dag', reportSource: 'mcp' },
|
|
481
501
|
reasonAsNote: true,
|
|
482
|
-
bodyFields: ['note', 'verdict', 'riskScore'],
|
|
502
|
+
bodyFields: ['note', 'verdict', 'riskScore', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
483
503
|
inputSchema: objectSchema({
|
|
484
504
|
notificationId: string('Notification id from the human action row.'),
|
|
485
505
|
ip: string('IP address to block.'),
|
|
486
506
|
note: string('Audit note explaining why the AI block was approved.'),
|
|
487
507
|
verdict: string('Optional final verdict text.'),
|
|
488
508
|
riskScore: number('Optional risk score.', { minimum: 0, maximum: 100 }),
|
|
509
|
+
...decisionReportInput,
|
|
510
|
+
...confirmSchema,
|
|
511
|
+
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'securenow_human_action_decision_report_add',
|
|
515
|
+
title: 'Record Human Action Decision Report',
|
|
516
|
+
description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
|
|
517
|
+
scope: 'notifications:write',
|
|
518
|
+
readOnly: false,
|
|
519
|
+
confirm: true,
|
|
520
|
+
method: 'POST',
|
|
521
|
+
endpoint: '/notifications/:notificationId/ips/:ip/decision-report',
|
|
522
|
+
pathParams: ['notificationId', 'ip'],
|
|
523
|
+
fixedBody: { reportSource: 'mcp' },
|
|
524
|
+
bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
525
|
+
inputSchema: objectSchema({
|
|
526
|
+
notificationId: string('Notification id from the human action row.'),
|
|
527
|
+
ip: string('IP address for the report.'),
|
|
528
|
+
...decisionReportInput,
|
|
489
529
|
...confirmSchema,
|
|
490
530
|
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
491
531
|
},
|
|
492
532
|
{
|
|
493
533
|
name: 'securenow_human_action_false_positive',
|
|
494
534
|
title: 'Mark Human Action False Positive',
|
|
495
|
-
description: 'Mark an AI-prepared human action as a scoped false positive. Write action; requires confirmation.',
|
|
535
|
+
description: 'Mark an AI-prepared human action as a scoped false positive and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
|
|
496
536
|
scope: 'notifications:write',
|
|
497
537
|
readOnly: false,
|
|
498
538
|
confirm: true,
|
|
499
539
|
method: 'POST',
|
|
500
540
|
endpoint: '/notifications/:notificationId/ips/:ip/false-positive',
|
|
501
541
|
pathParams: ['notificationId', 'ip'],
|
|
502
|
-
|
|
542
|
+
fixedBody: { reportSource: 'mcp' },
|
|
543
|
+
bodyFields: ['reason', 'conditions', 'matchMode', 'createExclusion', 'applyToExisting', 'ruleScope', 'targetRuleIds', 'aiConfidence', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
503
544
|
inputSchema: objectSchema({
|
|
504
545
|
notificationId: string('Notification id from the human action row.'),
|
|
505
546
|
ip: string('IP address to mark as false positive.'),
|
|
@@ -510,6 +551,7 @@ const TOOLS = [
|
|
|
510
551
|
ruleScope: string('Scope: this_rule, specific_rules, all_existing, or any_rule.'),
|
|
511
552
|
targetRuleIds: arrayOfStrings('Specific rule ids when ruleScope is specific_rules.'),
|
|
512
553
|
aiConfidence: number('AI confidence for audit metadata.', { minimum: 0, maximum: 100 }),
|
|
554
|
+
...decisionReportInput,
|
|
513
555
|
...confirmSchema,
|
|
514
556
|
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
515
557
|
},
|
|
@@ -523,16 +565,36 @@ const TOOLS = [
|
|
|
523
565
|
method: 'PUT',
|
|
524
566
|
endpoint: '/notifications/:notificationId/agent-case/actions/:actionKey',
|
|
525
567
|
pathParams: ['notificationId', 'actionKey'],
|
|
526
|
-
|
|
568
|
+
fixedBody: { reportSource: 'mcp' },
|
|
569
|
+
bodyFields: ['status', 'result', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
527
570
|
reasonInResult: true,
|
|
528
571
|
inputSchema: objectSchema({
|
|
529
572
|
notificationId: string('Notification id from the case-action row.'),
|
|
530
573
|
actionKey: string('Proposed action key from the row, for example tune_rule:...'),
|
|
531
574
|
status: string('New status: proposed, approved, rejected, executed, or failed.'),
|
|
532
575
|
result: { type: 'object', additionalProperties: true, description: 'Optional structured result/audit details.' },
|
|
576
|
+
...decisionReportInput,
|
|
533
577
|
...confirmSchema,
|
|
534
578
|
}, ['notificationId', 'actionKey', 'status', 'confirm', 'reason']),
|
|
535
579
|
},
|
|
580
|
+
{
|
|
581
|
+
name: 'securenow_human_case_decision_report_add',
|
|
582
|
+
title: 'Record Case Decision Report',
|
|
583
|
+
description: 'Attach a structured analyst/MCP decision report to a notification/case without changing IP status or proposed action state. Write action; requires confirmation.',
|
|
584
|
+
scope: 'notifications:write',
|
|
585
|
+
readOnly: false,
|
|
586
|
+
confirm: true,
|
|
587
|
+
method: 'POST',
|
|
588
|
+
endpoint: '/notifications/:notificationId/decision-report',
|
|
589
|
+
pathParams: ['notificationId'],
|
|
590
|
+
fixedBody: { reportSource: 'mcp' },
|
|
591
|
+
bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
592
|
+
inputSchema: objectSchema({
|
|
593
|
+
notificationId: string('Notification id from the case-action row.'),
|
|
594
|
+
...decisionReportInput,
|
|
595
|
+
...confirmSchema,
|
|
596
|
+
}, ['notificationId', 'confirm', 'reason']),
|
|
597
|
+
},
|
|
536
598
|
{
|
|
537
599
|
name: 'securenow_ip_lookup',
|
|
538
600
|
title: 'IP Intelligence Lookup',
|
|
@@ -614,6 +676,21 @@ const TOOLS = [
|
|
|
614
676
|
endpoint: '/automation-rules',
|
|
615
677
|
inputSchema: objectSchema({}),
|
|
616
678
|
},
|
|
679
|
+
{
|
|
680
|
+
name: 'securenow_automation_defaults_ensure',
|
|
681
|
+
title: 'Ensure Default Automation Rules',
|
|
682
|
+
description: 'Create or refresh SecureNow default risk-score automation rules for the current account. Write action; requires confirmation.',
|
|
683
|
+
scope: 'automation:write',
|
|
684
|
+
readOnly: false,
|
|
685
|
+
confirm: true,
|
|
686
|
+
method: 'POST',
|
|
687
|
+
endpoint: '/automation-rules/defaults/ensure',
|
|
688
|
+
bodyFields: ['forceEnableExisting'],
|
|
689
|
+
inputSchema: objectSchema({
|
|
690
|
+
forceEnableExisting: boolean('Re-enable existing SecureNow default rules if the customer disabled them. Defaults to false.'),
|
|
691
|
+
...confirmSchema,
|
|
692
|
+
}, ['confirm', 'reason']),
|
|
693
|
+
},
|
|
617
694
|
{
|
|
618
695
|
name: 'securenow_automation_rule_get',
|
|
619
696
|
title: 'Get Automation Rule',
|
|
@@ -636,13 +713,14 @@ const TOOLS = [
|
|
|
636
713
|
confirm: true,
|
|
637
714
|
method: 'POST',
|
|
638
715
|
endpoint: '/automation-rules',
|
|
639
|
-
bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
|
|
716
|
+
bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
|
|
640
717
|
inputSchema: objectSchema({
|
|
641
718
|
name: string('Rule name.'),
|
|
642
719
|
description: string('Optional rule description.'),
|
|
643
720
|
conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array. Fields include abuseConfidenceScore, riskScore, alertName, alertTag, attackType, path, environment.' },
|
|
644
721
|
conditionLogic: string('AND or OR.'),
|
|
645
722
|
actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array, for example [{ "type":"addToBlocklist", "config":{ "reason":"...", "ttlHours":24 }}].' },
|
|
723
|
+
status: string('Initial rule status: active or disabled. Defaults to active.'),
|
|
646
724
|
applicationsAll: boolean('Apply to all applications.'),
|
|
647
725
|
applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
|
|
648
726
|
environmentsAll: boolean('Apply to all environments.'),
|
|
@@ -864,13 +942,97 @@ const TOOLS = [
|
|
|
864
942
|
readOnly: true,
|
|
865
943
|
method: 'GET',
|
|
866
944
|
endpoint: '/blocklist',
|
|
867
|
-
queryFields: ['page', 'limit', 'appKey', 'environment'],
|
|
945
|
+
queryFields: ['page', 'limit', 'status', 'approvalStatus', 'search', 'view', 'appKey', 'environment'],
|
|
868
946
|
inputSchema: objectSchema({
|
|
869
947
|
...pagingInput,
|
|
948
|
+
status: string('Block entry status: active or removed. Defaults to active.'),
|
|
949
|
+
approvalStatus: string('Optional approval filter: pending, approved, or rejected.'),
|
|
950
|
+
search: string('Optional IP prefix search.'),
|
|
951
|
+
view: string('List view: all or operational.'),
|
|
870
952
|
appKey: string('Optional application key scope.'),
|
|
871
953
|
...environmentInput,
|
|
872
954
|
}),
|
|
873
955
|
},
|
|
956
|
+
{
|
|
957
|
+
name: 'securenow_blocklist_pending_list',
|
|
958
|
+
title: 'List Pending Block Approvals',
|
|
959
|
+
description: 'List legacy or AI-prepared blocklist entries with approvalStatus=pending. Pending blocks are not enforced until approved.',
|
|
960
|
+
scope: 'blocklist:read',
|
|
961
|
+
readOnly: true,
|
|
962
|
+
method: 'GET',
|
|
963
|
+
endpoint: '/blocklist/pending',
|
|
964
|
+
queryFields: ['page', 'limit', 'search', 'appKey', 'environment'],
|
|
965
|
+
inputSchema: objectSchema({
|
|
966
|
+
...pagingInput,
|
|
967
|
+
search: string('Optional IP prefix search.'),
|
|
968
|
+
appKey: string('Optional application key scope.'),
|
|
969
|
+
...environmentInput,
|
|
970
|
+
}),
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
name: 'securenow_blocklist_pending_approve',
|
|
974
|
+
title: 'Approve Pending Block',
|
|
975
|
+
description: 'Approve one pending blocklist entry so the firewall enforces it. Write action; requires confirmation.',
|
|
976
|
+
scope: 'blocklist:write',
|
|
977
|
+
readOnly: false,
|
|
978
|
+
destructive: true,
|
|
979
|
+
confirm: true,
|
|
980
|
+
method: 'POST',
|
|
981
|
+
endpoint: '/blocklist/:id/approve',
|
|
982
|
+
pathParams: ['id'],
|
|
983
|
+
bodyFields: ['reason'],
|
|
984
|
+
inputSchema: objectSchema({
|
|
985
|
+
id: string('Pending blocklist entry id.'),
|
|
986
|
+
...confirmSchema,
|
|
987
|
+
}, ['id', 'confirm', 'reason']),
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: 'securenow_blocklist_pending_reject',
|
|
991
|
+
title: 'Reject Pending Block',
|
|
992
|
+
description: 'Reject one pending blocklist entry and remove it from enforcement consideration. Write action; requires confirmation.',
|
|
993
|
+
scope: 'blocklist:write',
|
|
994
|
+
readOnly: false,
|
|
995
|
+
confirm: true,
|
|
996
|
+
method: 'POST',
|
|
997
|
+
endpoint: '/blocklist/:id/reject',
|
|
998
|
+
pathParams: ['id'],
|
|
999
|
+
bodyFields: ['reason'],
|
|
1000
|
+
inputSchema: objectSchema({
|
|
1001
|
+
id: string('Pending blocklist entry id.'),
|
|
1002
|
+
...confirmSchema,
|
|
1003
|
+
}, ['id', 'confirm', 'reason']),
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'securenow_blocklist_pending_bulk_approve',
|
|
1007
|
+
title: 'Bulk Approve Pending Blocks',
|
|
1008
|
+
description: 'Approve multiple pending blocklist entries after they have all been reviewed under the same safe policy. Write action; requires confirmation.',
|
|
1009
|
+
scope: 'blocklist:write',
|
|
1010
|
+
readOnly: false,
|
|
1011
|
+
destructive: true,
|
|
1012
|
+
confirm: true,
|
|
1013
|
+
method: 'POST',
|
|
1014
|
+
endpoint: '/blocklist/bulk-approve',
|
|
1015
|
+
bodyFields: ['ids', 'reason'],
|
|
1016
|
+
inputSchema: objectSchema({
|
|
1017
|
+
ids: arrayOfStrings('Pending blocklist entry ids to approve.'),
|
|
1018
|
+
...confirmSchema,
|
|
1019
|
+
}, ['ids', 'confirm', 'reason']),
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: 'securenow_blocklist_pending_bulk_reject',
|
|
1023
|
+
title: 'Bulk Reject Pending Blocks',
|
|
1024
|
+
description: 'Reject multiple pending blocklist entries after they have all been reviewed as false positives or stale/ambiguous. Write action; requires confirmation.',
|
|
1025
|
+
scope: 'blocklist:write',
|
|
1026
|
+
readOnly: false,
|
|
1027
|
+
confirm: true,
|
|
1028
|
+
method: 'POST',
|
|
1029
|
+
endpoint: '/blocklist/bulk-reject',
|
|
1030
|
+
bodyFields: ['ids', 'reason'],
|
|
1031
|
+
inputSchema: objectSchema({
|
|
1032
|
+
ids: arrayOfStrings('Pending blocklist entry ids to reject.'),
|
|
1033
|
+
...confirmSchema,
|
|
1034
|
+
}, ['ids', 'confirm', 'reason']),
|
|
1035
|
+
},
|
|
874
1036
|
{
|
|
875
1037
|
name: 'securenow_blocklist_add',
|
|
876
1038
|
title: 'Add Blocked IP',
|
|
@@ -893,14 +1055,31 @@ const TOOLS = [
|
|
|
893
1055
|
},
|
|
894
1056
|
{
|
|
895
1057
|
name: 'securenow_blocklist_remove',
|
|
896
|
-
title: '
|
|
897
|
-
description: '
|
|
1058
|
+
title: 'Unblock IP (Compat Alias)',
|
|
1059
|
+
description: 'Compatibility alias for unblocking a blocklist entry while retaining block report/history. Write action; requires confirmation.',
|
|
898
1060
|
scope: 'blocklist:write',
|
|
899
1061
|
readOnly: false,
|
|
900
1062
|
confirm: true,
|
|
901
|
-
method: '
|
|
902
|
-
endpoint: '/blocklist/:id',
|
|
1063
|
+
method: 'POST',
|
|
1064
|
+
endpoint: '/blocklist/:id/unblock',
|
|
1065
|
+
pathParams: ['id'],
|
|
1066
|
+
bodyFields: ['reason'],
|
|
1067
|
+
inputSchema: objectSchema({
|
|
1068
|
+
id: string('Blocklist entry id.'),
|
|
1069
|
+
...confirmSchema,
|
|
1070
|
+
}, ['id', 'confirm', 'reason']),
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: 'securenow_blocklist_unblock',
|
|
1074
|
+
title: 'Unblock IP And Keep History',
|
|
1075
|
+
description: 'Unblock a blocklist entry so firewall enforcement stops, while retaining the block report, history, and unblock audit trail. Write action; requires confirmation.',
|
|
1076
|
+
scope: 'blocklist:write',
|
|
1077
|
+
readOnly: false,
|
|
1078
|
+
confirm: true,
|
|
1079
|
+
method: 'POST',
|
|
1080
|
+
endpoint: '/blocklist/:id/unblock',
|
|
903
1081
|
pathParams: ['id'],
|
|
1082
|
+
bodyFields: ['reason'],
|
|
904
1083
|
inputSchema: objectSchema({
|
|
905
1084
|
id: string('Blocklist entry id.'),
|
|
906
1085
|
...confirmSchema,
|
|
@@ -1114,6 +1293,25 @@ const PROMPTS = [
|
|
|
1114
1293
|
{ name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
|
|
1115
1294
|
],
|
|
1116
1295
|
},
|
|
1296
|
+
{
|
|
1297
|
+
name: 'cleanup_legacy_pending_blocks',
|
|
1298
|
+
title: 'Clean Legacy Pending Blocks',
|
|
1299
|
+
description: 'Use MCP tools to review and approve/reject legacy pending blocklist rows so only current human work remains.',
|
|
1300
|
+
arguments: [
|
|
1301
|
+
{ name: 'limit', description: 'Maximum pending block rows to review this run. Defaults to 50.', required: false },
|
|
1302
|
+
{ name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
|
|
1303
|
+
{ name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute approvals/rejections.', required: false },
|
|
1304
|
+
],
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
name: 'configure_default_automation',
|
|
1308
|
+
title: 'Configure Default Automation',
|
|
1309
|
+
description: 'Use MCP tools to set conservative default SecureNow automation rules based on the canonical riskScore and supporting evidence.',
|
|
1310
|
+
arguments: [
|
|
1311
|
+
{ name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
|
|
1312
|
+
{ name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to create/update automation rules.', required: false },
|
|
1313
|
+
],
|
|
1314
|
+
},
|
|
1117
1315
|
];
|
|
1118
1316
|
|
|
1119
1317
|
function promptMessages(name, args = {}) {
|
|
@@ -1186,11 +1384,11 @@ function promptMessages(name, args = {}) {
|
|
|
1186
1384
|
text: [
|
|
1187
1385
|
`Investigate Requires Human row ${rowNumber} using SecureNow MCP.`,
|
|
1188
1386
|
`Fetch page=${page}, limit=${limit} with securenow_human_actions_list, select row ${rowNumber}, then call securenow_notifications_get and securenow_human_action_report for that notificationId and IP.`,
|
|
1189
|
-
'Read the AI report, finalDecision,
|
|
1387
|
+
'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
1190
1388
|
'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
|
|
1191
1389
|
'Return one clear outcome: Block IP, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
|
|
1192
1390
|
confirmWrites
|
|
1193
|
-
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block or securenow_human_action_false_positive with confirm:true and a
|
|
1391
|
+
? 'The user requested execution. If evidence supports the decision, call securenow_human_action_block or securenow_human_action_false_positive with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant. If you skip/mark ambiguous but still need to record the audit trail, call securenow_human_action_decision_report_add.'
|
|
1194
1392
|
: 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
|
|
1195
1393
|
'If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
1196
1394
|
'False positives must be narrow: app + alert rule + path + method/status/user-agent/body evidence where possible. Never globally trust an IP by default.',
|
|
@@ -1211,10 +1409,10 @@ function promptMessages(name, args = {}) {
|
|
|
1211
1409
|
text: [
|
|
1212
1410
|
'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
|
|
1213
1411
|
`Review up to ${limit} row(s), most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
|
|
1214
|
-
'Start with securenow_human_actions_list. For each row, call securenow_notifications_get and securenow_human_action_report, inspect the AI report/
|
|
1412
|
+
'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.',
|
|
1215
1413
|
'For each row choose exactly one outcome: Block IP, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
|
|
1216
1414
|
confirmWrites
|
|
1217
|
-
? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true
|
|
1415
|
+
? '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/ambiguous/rule-tuning-needed rows that should be auditable without changing IP status, use securenow_human_action_decision_report_add.'
|
|
1218
1416
|
: 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
|
|
1219
1417
|
'For block decisions, use securenow_human_action_block. 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 only when the action is safe to approve/reject.',
|
|
1220
1418
|
'End with counts: handled, proposed block, proposed false positive, rule tuning needed, skipped, still waiting.',
|
|
@@ -1224,6 +1422,62 @@ function promptMessages(name, args = {}) {
|
|
|
1224
1422
|
];
|
|
1225
1423
|
}
|
|
1226
1424
|
|
|
1425
|
+
if (name === 'cleanup_legacy_pending_blocks') {
|
|
1426
|
+
const limit = args.limit || 50;
|
|
1427
|
+
const environment = args.environment || 'production';
|
|
1428
|
+
const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
|
|
1429
|
+
return [
|
|
1430
|
+
{
|
|
1431
|
+
role: 'user',
|
|
1432
|
+
content: {
|
|
1433
|
+
type: 'text',
|
|
1434
|
+
text: [
|
|
1435
|
+
'Clean my SecureNow legacy pending blocklist queue using MCP tools.',
|
|
1436
|
+
`Review up to ${limit} pending block row(s). Environment scope: ${environment}.`,
|
|
1437
|
+
'Start with securenow_blocklist_stats, then securenow_blocklist_pending_list({ page: 1, limit, environment }).',
|
|
1438
|
+
'For each pending block, inspect id, IP, source, reason, metadata.riskScore, metadata.aiRiskScore, metadata.abuseConfidenceScore, automation rule, linked notification, age, app, and environment.',
|
|
1439
|
+
'When investigationNotificationId exists, fetch securenow_notifications_get for that case and use the linked IP report/history as evidence.',
|
|
1440
|
+
'Approve only when the row has clear malicious evidence, riskScore >= 90, or SecureNow IPDB evidence score >= 80 with no false-positive/test signal.',
|
|
1441
|
+
'Reject stale, ambiguous, synthetic/test, self-traffic, or false-positive rows. Prefer narrow rule/exclusion tuning when the same benign pattern repeats.',
|
|
1442
|
+
confirmWrites
|
|
1443
|
+
? 'The user requested execution. Use securenow_blocklist_pending_approve or securenow_blocklist_pending_reject with confirm:true and a precise reason. Use bulk tools only after every selected row satisfies the same reviewed policy.'
|
|
1444
|
+
: 'Do not execute writes yet. Produce exact approve/reject MCP calls grouped by safe policy for user approval.',
|
|
1445
|
+
'After each write or batch, re-fetch securenow_blocklist_stats and securenow_blocklist_pending_list until the legacy pending count is zero or only ambiguous rows remain.',
|
|
1446
|
+
'End with counts: approved, rejected, skipped ambiguous, legacyPendingBlockCount, and remaining proof gaps.',
|
|
1447
|
+
].join('\n'),
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
];
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (name === 'configure_default_automation') {
|
|
1454
|
+
const environment = args.environment || 'production';
|
|
1455
|
+
const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
|
|
1456
|
+
return [
|
|
1457
|
+
{
|
|
1458
|
+
role: 'user',
|
|
1459
|
+
content: {
|
|
1460
|
+
type: 'text',
|
|
1461
|
+
text: [
|
|
1462
|
+
'Configure SecureNow default automation using the MCP tools.',
|
|
1463
|
+
`Review environment context: ${environment}. Built-in defaults apply to all apps and all environments unless the customer narrows them later.`,
|
|
1464
|
+
'Use one canonical product score for automation decisions: riskScore. SecureNow IPDB / AbuseIPDB score and AI confidence remain supporting evidence.',
|
|
1465
|
+
'Built-in defaults are active by default:',
|
|
1466
|
+
'- Critical Risk Auto-Block: riskScore>=95, TTL 168h.',
|
|
1467
|
+
'- High Risk Auto-Block: riskScore>=90 AND riskScore<95, TTL 72h.',
|
|
1468
|
+
'- Elevated Risk Auto-Block: riskScore>=85 AND riskScore<90, TTL 24h.',
|
|
1469
|
+
'Start with securenow_automation_defaults_ensure({ confirm:true, reason:"Provision SecureNow default risk-score automation" }) when writes are confirmed, otherwise start with securenow_automation_rules_list and report what would be ensured.',
|
|
1470
|
+
'After ensuring defaults, dry-run each default rule with securenow_automation_rule_dry_run to inspect sample matches. Disable or tune only if evidence shows customer-specific false positives.',
|
|
1471
|
+
confirmWrites
|
|
1472
|
+
? 'The user requested execution. Ensure defaults with confirm:true, do not force-enable previously disabled defaults unless the user explicitly asked, and re-list rules after writes.'
|
|
1473
|
+
: 'Do not execute writes yet. Return exact MCP update/create calls and dry-run calls for approval.',
|
|
1474
|
+
'End with active rules, disabled rules that should stay disabled, and any risk from automation scope.',
|
|
1475
|
+
].join('\n'),
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
];
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1227
1481
|
throw new Error(`Unknown prompt: ${name}`);
|
|
1228
1482
|
}
|
|
1229
1483
|
|
package/package.json
CHANGED