securenow 7.7.5 → 7.7.6

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
@@ -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
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"
@@ -304,6 +304,16 @@ securenow blocklist remove <id>
304
304
  securenow blocklist stats # block counts, top reasons
305
305
  ```
306
306
 
307
+ MCP exposes legacy pending-block cleanup separately from current Requires Human
308
+ work:
309
+
310
+ - `securenow_blocklist_pending_list`
311
+ - `securenow_blocklist_pending_approve`
312
+ - `securenow_blocklist_pending_reject`
313
+ - `securenow_blocklist_pending_bulk_approve`
314
+ - `securenow_blocklist_pending_bulk_reject`
315
+ - prompt: `cleanup_legacy_pending_blocks`
316
+
307
317
  ### Automation Rules
308
318
 
309
319
  ```bash
@@ -312,6 +322,11 @@ securenow automation show <id>
312
322
  securenow automation dry-run <id> --limit 500
313
323
  securenow automation execute <id> --yes
314
324
  ```
325
+
326
+ For default automation, prefer production-scoped `riskScore` policies:
327
+ `riskScore >= 90` for autonomous AI malicious blocking, `riskScore >= 80`
328
+ plus `alertTag in xss` for short-lived XSS blocks, and keep raw SecureNow IPDB
329
+ confidence as supporting reputation evidence.
315
330
 
316
331
  ### Allowlist — Restrict to Known IPs
317
332
 
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
@@ -926,7 +926,7 @@ async function ipLookup(args, flags) {
926
926
  if (data.domain) pairs.push(['Domain', data.domain]);
927
927
  if (data.isp) pairs.push(['ISP', data.isp]);
928
928
  if (data.usageType) pairs.push(['Usage Type', data.usageType]);
929
- if (data.abuseConfidenceScore != null) pairs.push(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
929
+ if (data.abuseConfidenceScore != null) pairs.push(['SecureNow IPDB Score', `${data.abuseConfidenceScore}/100`]);
930
930
  if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
931
931
  if (data.verdict) pairs.push(['Verdict', data.verdict]);
932
932
  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) },
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',
@@ -636,13 +698,14 @@ const TOOLS = [
636
698
  confirm: true,
637
699
  method: 'POST',
638
700
  endpoint: '/automation-rules',
639
- bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
701
+ bodyFields: ['name', 'description', 'conditions', 'conditionLogic', 'actions', 'status', 'applicationsAll', 'applicationKeys', 'environmentsAll', 'environments'],
640
702
  inputSchema: objectSchema({
641
703
  name: string('Rule name.'),
642
704
  description: string('Optional rule description.'),
643
705
  conditions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Condition array. Fields include abuseConfidenceScore, riskScore, alertName, alertTag, attackType, path, environment.' },
644
706
  conditionLogic: string('AND or OR.'),
645
707
  actions: { type: 'array', items: { type: 'object', additionalProperties: true }, description: 'Action array, for example [{ "type":"addToBlocklist", "config":{ "reason":"...", "ttlHours":24 }}].' },
708
+ status: string('Initial rule status: active or disabled. Defaults to active.'),
646
709
  applicationsAll: boolean('Apply to all applications.'),
647
710
  applicationKeys: arrayOfStrings('Application keys when not applying to all applications.'),
648
711
  environmentsAll: boolean('Apply to all environments.'),
@@ -864,13 +927,97 @@ const TOOLS = [
864
927
  readOnly: true,
865
928
  method: 'GET',
866
929
  endpoint: '/blocklist',
867
- queryFields: ['page', 'limit', 'appKey', 'environment'],
930
+ queryFields: ['page', 'limit', 'status', 'approvalStatus', 'search', 'view', 'appKey', 'environment'],
868
931
  inputSchema: objectSchema({
869
932
  ...pagingInput,
933
+ status: string('Block entry status: active or removed. Defaults to active.'),
934
+ approvalStatus: string('Optional approval filter: pending, approved, or rejected.'),
935
+ search: string('Optional IP prefix search.'),
936
+ view: string('List view: all or operational.'),
870
937
  appKey: string('Optional application key scope.'),
871
938
  ...environmentInput,
872
939
  }),
873
940
  },
941
+ {
942
+ name: 'securenow_blocklist_pending_list',
943
+ title: 'List Pending Block Approvals',
944
+ description: 'List legacy or AI-prepared blocklist entries with approvalStatus=pending. Pending blocks are not enforced until approved.',
945
+ scope: 'blocklist:read',
946
+ readOnly: true,
947
+ method: 'GET',
948
+ endpoint: '/blocklist/pending',
949
+ queryFields: ['page', 'limit', 'search', 'appKey', 'environment'],
950
+ inputSchema: objectSchema({
951
+ ...pagingInput,
952
+ search: string('Optional IP prefix search.'),
953
+ appKey: string('Optional application key scope.'),
954
+ ...environmentInput,
955
+ }),
956
+ },
957
+ {
958
+ name: 'securenow_blocklist_pending_approve',
959
+ title: 'Approve Pending Block',
960
+ description: 'Approve one pending blocklist entry so the firewall enforces it. Write action; requires confirmation.',
961
+ scope: 'blocklist:write',
962
+ readOnly: false,
963
+ destructive: true,
964
+ confirm: true,
965
+ method: 'POST',
966
+ endpoint: '/blocklist/:id/approve',
967
+ pathParams: ['id'],
968
+ bodyFields: ['reason'],
969
+ inputSchema: objectSchema({
970
+ id: string('Pending blocklist entry id.'),
971
+ ...confirmSchema,
972
+ }, ['id', 'confirm', 'reason']),
973
+ },
974
+ {
975
+ name: 'securenow_blocklist_pending_reject',
976
+ title: 'Reject Pending Block',
977
+ description: 'Reject one pending blocklist entry and remove it from enforcement consideration. Write action; requires confirmation.',
978
+ scope: 'blocklist:write',
979
+ readOnly: false,
980
+ confirm: true,
981
+ method: 'POST',
982
+ endpoint: '/blocklist/:id/reject',
983
+ pathParams: ['id'],
984
+ bodyFields: ['reason'],
985
+ inputSchema: objectSchema({
986
+ id: string('Pending blocklist entry id.'),
987
+ ...confirmSchema,
988
+ }, ['id', 'confirm', 'reason']),
989
+ },
990
+ {
991
+ name: 'securenow_blocklist_pending_bulk_approve',
992
+ title: 'Bulk Approve Pending Blocks',
993
+ description: 'Approve multiple pending blocklist entries after they have all been reviewed under the same safe policy. Write action; requires confirmation.',
994
+ scope: 'blocklist:write',
995
+ readOnly: false,
996
+ destructive: true,
997
+ confirm: true,
998
+ method: 'POST',
999
+ endpoint: '/blocklist/bulk-approve',
1000
+ bodyFields: ['ids', 'reason'],
1001
+ inputSchema: objectSchema({
1002
+ ids: arrayOfStrings('Pending blocklist entry ids to approve.'),
1003
+ ...confirmSchema,
1004
+ }, ['ids', 'confirm', 'reason']),
1005
+ },
1006
+ {
1007
+ name: 'securenow_blocklist_pending_bulk_reject',
1008
+ title: 'Bulk Reject Pending Blocks',
1009
+ description: 'Reject multiple pending blocklist entries after they have all been reviewed as false positives or stale/ambiguous. Write action; requires confirmation.',
1010
+ scope: 'blocklist:write',
1011
+ readOnly: false,
1012
+ confirm: true,
1013
+ method: 'POST',
1014
+ endpoint: '/blocklist/bulk-reject',
1015
+ bodyFields: ['ids', 'reason'],
1016
+ inputSchema: objectSchema({
1017
+ ids: arrayOfStrings('Pending blocklist entry ids to reject.'),
1018
+ ...confirmSchema,
1019
+ }, ['ids', 'confirm', 'reason']),
1020
+ },
874
1021
  {
875
1022
  name: 'securenow_blocklist_add',
876
1023
  title: 'Add Blocked IP',
@@ -1114,6 +1261,25 @@ const PROMPTS = [
1114
1261
  { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
1115
1262
  ],
1116
1263
  },
1264
+ {
1265
+ name: 'cleanup_legacy_pending_blocks',
1266
+ title: 'Clean Legacy Pending Blocks',
1267
+ description: 'Use MCP tools to review and approve/reject legacy pending blocklist rows so only current human work remains.',
1268
+ arguments: [
1269
+ { name: 'limit', description: 'Maximum pending block rows to review this run. Defaults to 50.', required: false },
1270
+ { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1271
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute approvals/rejections.', required: false },
1272
+ ],
1273
+ },
1274
+ {
1275
+ name: 'configure_default_automation',
1276
+ title: 'Configure Default Automation',
1277
+ description: 'Use MCP tools to set conservative default SecureNow automation rules based on the canonical riskScore and supporting evidence.',
1278
+ arguments: [
1279
+ { name: 'environment', description: 'Environment scope. Defaults to production.', required: false },
1280
+ { name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to create/update automation rules.', required: false },
1281
+ ],
1282
+ },
1117
1283
  ];
1118
1284
 
1119
1285
  function promptMessages(name, args = {}) {
@@ -1186,11 +1352,11 @@ function promptMessages(name, args = {}) {
1186
1352
  text: [
1187
1353
  `Investigate Requires Human row ${rowNumber} using SecureNow MCP.`,
1188
1354
  `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.',
1355
+ 'Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
1190
1356
  'Open trace evidence with securenow_traces_show and correlated logs with securenow_logs_for_trace when trace IDs are available.',
1191
1357
  'Return one clear outcome: Block IP, False Positive, Rule Tuning Needed, or Ambiguous. If evidence is ambiguous, stop and explain what is missing.',
1192
1358
  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.'
1359
+ ? '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
1360
  : 'Do not execute write tools yet. Prepare the recommended decision and exact tool call the user can approve.',
1195
1361
  '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
1362
  '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 +1377,10 @@ function promptMessages(name, args = {}) {
1211
1377
  text: [
1212
1378
  'Work my SecureNow Requires Human queue like a senior security analyst using the MCP tools.',
1213
1379
  `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.',
1380
+ '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
1381
  'For each row choose exactly one outcome: Block IP, False Positive, Rule Tuning Needed, or Skip because evidence is insufficient. Explain skipped rows.',
1216
1382
  confirmWrites
1217
- ? 'The user requested execution. For supported decisions, call the correct write tool with confirm:true and a precise reason, then continue.'
1383
+ ? '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
1384
  : 'Do not execute write tools yet. Produce a row-by-row action plan and exact MCP write calls for user approval.',
1219
1385
  '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
1386
  'End with counts: handled, proposed block, proposed false positive, rule tuning needed, skipped, still waiting.',
@@ -1224,6 +1390,62 @@ function promptMessages(name, args = {}) {
1224
1390
  ];
1225
1391
  }
1226
1392
 
1393
+ if (name === 'cleanup_legacy_pending_blocks') {
1394
+ const limit = args.limit || 50;
1395
+ const environment = args.environment || 'production';
1396
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
1397
+ return [
1398
+ {
1399
+ role: 'user',
1400
+ content: {
1401
+ type: 'text',
1402
+ text: [
1403
+ 'Clean my SecureNow legacy pending blocklist queue using MCP tools.',
1404
+ `Review up to ${limit} pending block row(s). Environment scope: ${environment}.`,
1405
+ 'Start with securenow_blocklist_stats, then securenow_blocklist_pending_list({ page: 1, limit, environment }).',
1406
+ 'For each pending block, inspect id, IP, source, reason, metadata.riskScore, metadata.aiRiskScore, metadata.abuseConfidenceScore, automation rule, linked notification, age, app, and environment.',
1407
+ 'When investigationNotificationId exists, fetch securenow_notifications_get for that case and use the linked IP report/history as evidence.',
1408
+ 'Approve only when the row has clear malicious evidence, riskScore >= 90, or SecureNow IPDB evidence score >= 80 with no false-positive/test signal.',
1409
+ 'Reject stale, ambiguous, synthetic/test, self-traffic, or false-positive rows. Prefer narrow rule/exclusion tuning when the same benign pattern repeats.',
1410
+ confirmWrites
1411
+ ? '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.'
1412
+ : 'Do not execute writes yet. Produce exact approve/reject MCP calls grouped by safe policy for user approval.',
1413
+ '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.',
1414
+ 'End with counts: approved, rejected, skipped ambiguous, legacyPendingBlockCount, and remaining proof gaps.',
1415
+ ].join('\n'),
1416
+ },
1417
+ },
1418
+ ];
1419
+ }
1420
+
1421
+ if (name === 'configure_default_automation') {
1422
+ const environment = args.environment || 'production';
1423
+ const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
1424
+ return [
1425
+ {
1426
+ role: 'user',
1427
+ content: {
1428
+ type: 'text',
1429
+ text: [
1430
+ 'Configure SecureNow default automation using the MCP tools.',
1431
+ `Default environment scope: ${environment}.`,
1432
+ 'Use one canonical product score for automation decisions: riskScore. Treat SecureNow IPDB / AbuseIPDB score and AI confidence as supporting evidence, not the primary UI score.',
1433
+ 'Desired defaults:',
1434
+ '- Auto-block High-Confidence AI Malicious IPs: aiDecision=pending_approval AND isMalicious=true AND riskScore>=90, TTL 168h.',
1435
+ '- Auto-block High-Confidence XSS IPs: alertTag in xss AND riskScore>=80, TTL 24h.',
1436
+ '- High-reputation IPDB automation may remain enabled only as an explicit reputation policy: abuseConfidenceScore>=80.',
1437
+ 'Start with securenow_automation_rules_list. Reuse/update existing rules where names/conditions match instead of creating duplicates.',
1438
+ 'Create new rules with status=disabled when possible, dry-run each changed or created rule with securenow_automation_rule_dry_run, then enable only after the sample matches look safe.',
1439
+ confirmWrites
1440
+ ? 'The user requested execution. Create/update rules with confirm:true, production scope, clear names, and precise reasons. For new rules, prefer status=disabled, dry-run, then update status=active after review. Re-list rules after writes.'
1441
+ : 'Do not execute writes yet. Return exact MCP update/create calls and dry-run calls for approval.',
1442
+ 'End with active rules, disabled rules that should stay disabled, and any risk from automation scope.',
1443
+ ].join('\n'),
1444
+ },
1445
+ },
1446
+ ];
1447
+ }
1448
+
1227
1449
  throw new Error(`Unknown prompt: ${name}`);
1228
1450
  }
1229
1451
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "7.7.5",
3
+ "version": "7.7.6",
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",