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 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, abuse score, verdict, risk factors
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 remove <id>
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 remove <id>` | Unblock IP |
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, DAG, proofs, trace links
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 remove <id>
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('DAG steps')}`);
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, DAG steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
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 precise reason.',
424
- '7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, and a precise reason.',
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. 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.',
427
- '10. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
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 remove <id>');
396
- process.exit(1);
397
- }
398
-
399
- if (!flags.force && !flags.yes) {
400
- const ok = await ui.confirm('Remove this IP from blocklist?');
401
- if (!ok) { ui.info('Cancelled'); return; }
402
- }
403
-
404
- const s = ui.spinner('Removing from blocklist');
405
- try {
406
- await api.delete(`/blocklist/${id}`);
407
- s.stop('Removed from blocklist');
408
- } catch (err) {
409
- s.fail('Failed to remove from blocklist');
410
- throw err;
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(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
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, DAG, and trace links', usage: 'securenow human show <row|notificationId:ip>', run: (a, f) => require('./cli/human').show(a, f) },
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
- remove: { desc: 'Unblock an IP', usage: 'securenow blocklist remove <id>', run: (a, f) => require('./cli/security').blocklistRemove(a, f) },
290
- stats: { desc: 'Blocklist statistics', run: (a, f) => require('./cli/security').blocklistStats(a, f) },
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 abuse confidence score.', { minimum: 0, maximum: 100 }),
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, DAG steps, proofs, metadata, and AI decision for one human action row.',
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
- bodyFields: ['reason', 'conditions', 'matchMode', 'createExclusion', 'applyToExisting', 'ruleScope', 'targetRuleIds', 'aiConfidence'],
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
- bodyFields: ['status', 'result'],
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: 'Remove Blocked IP',
897
- description: 'Remove a blocklist entry. Write action; requires confirmation.',
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: 'DELETE',
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, DAG steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
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 precise reason.'
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/DAG/proofs/trace IDs, and fetch trace/log evidence where useful.',
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 and a precise reason, then continue.'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.5",
3
+ "version": "7.7.7",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",