securenow 7.7.4 → 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 +12 -3
- package/SKILL-API.md +2 -2
- package/SKILL-CLI.md +16 -1
- package/app-config.js +2 -2
- package/cli/diagnostics.js +80 -0
- package/cli/human.js +123 -7
- package/cli/security.js +1 -1
- package/cli.js +12 -2
- package/mcp/catalog.js +236 -14
- package/package.json +2 -2
- package/tracing.js +2 -2
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,
|
|
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
|
|
@@ -1261,7 +1261,7 @@ Use `npx securenow help firewall` for complete details on all layers.
|
|
|
1261
1261
|
|
|
1262
1262
|
| Variable | Description | Default |
|
|
1263
1263
|
|----------|-------------|---------|
|
|
1264
|
-
| `OTEL_LOG_LEVEL` | OpenTelemetry
|
|
1264
|
+
| `OTEL_LOG_LEVEL` | OpenTelemetry diagnostic override. Options: `debug`, `info`, `warn`, `error`, `none`. Overrides `config.otel.logLevel` for emergency debugging. | `error` |
|
|
1265
1265
|
| `SECURENOW_TEST_SPAN` | Set to `1` to emit a test span on startup. | `0` |
|
|
1266
1266
|
|
|
1267
1267
|
#### Environment
|
|
@@ -1600,7 +1600,16 @@ curl http://localhost:4318/v1/traces
|
|
|
1600
1600
|
# Should return 200 or 405 (method not allowed)
|
|
1601
1601
|
```
|
|
1602
1602
|
|
|
1603
|
-
**Check 3:
|
|
1603
|
+
**Check 3: Run doctor and enable debug diagnostics**
|
|
1604
|
+
|
|
1605
|
+
```bash
|
|
1606
|
+
npx securenow doctor --json
|
|
1607
|
+
OTEL_LOG_LEVEL=debug node -r securenow/register app.js
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
Doctor checks OTLP reachability and flags duplicate `@opentelemetry/api` versions that can silently leave tracing on the OpenTelemetry noop provider.
|
|
1611
|
+
|
|
1612
|
+
For a persistent setting, update credentials:
|
|
1604
1613
|
|
|
1605
1614
|
```json
|
|
1606
1615
|
{
|
package/SKILL-API.md
CHANGED
|
@@ -495,7 +495,7 @@ Local development and production use `.securenow/credentials.json`. Every settin
|
|
|
495
495
|
| `SECURENOW_DISABLE_INSTRUMENTATIONS` | Comma-separated packages to skip (e.g. `fs,dns`) | — |
|
|
496
496
|
| `SECURENOW_TEST_SPAN` | `1` to emit a test span on startup | `0` |
|
|
497
497
|
| `SECURENOW_HIDE_BANNER` | `1` to suppress free-trial upgrade banner | `0` |
|
|
498
|
-
| `OTEL_LOG_LEVEL` | SDK
|
|
498
|
+
| `OTEL_LOG_LEVEL` | SDK diagnostic override: `error`, `warn`, `info`, `debug`, or `none` | `error` |
|
|
499
499
|
| `SECURENOW_ENVIRONMENT` / `SECURENOW_DEPLOYMENT_ENVIRONMENT` / `NODE_ENV` | Legacy fallback for `config.runtime.deploymentEnvironment` | `production` |
|
|
500
500
|
|
|
501
501
|
### Firewall
|
|
@@ -634,7 +634,7 @@ On startup, securenow logs its configuration:
|
|
|
634
634
|
[securenow] Firewall: synced 142 blocked IPs
|
|
635
635
|
```
|
|
636
636
|
|
|
637
|
-
Set `config.otel.logLevel` to `debug
|
|
637
|
+
Set `config.otel.logLevel` to `debug`, or temporarily run with `OTEL_LOG_LEVEL=debug`, and run `securenow doctor --json` to troubleshoot connectivity, duplicate OpenTelemetry API packages, and provider registration issues.
|
|
638
638
|
|
|
639
639
|
**CLI equivalent** (works without booting the SDK — useful when the app won't start):
|
|
640
640
|
|
package/SKILL-CLI.md
CHANGED
|
@@ -221,7 +221,7 @@ Use this for the same work shown in **Requires Human**: AI has already grouped a
|
|
|
221
221
|
```bash
|
|
222
222
|
securenow human # list human decisions, most urgent first
|
|
223
223
|
securenow human list --limit 20
|
|
224
|
-
securenow human show 1 # inspect row 1 with AI report,
|
|
224
|
+
securenow human show 1 # inspect row 1 with AI report, investigation steps, proofs, trace links
|
|
225
225
|
securenow human block 1 --yes --reason "AI evidence confirmed malicious"
|
|
226
226
|
securenow human fp 1 --yes --reason "Scoped false positive after evidence review"
|
|
227
227
|
securenow human action 1 --status rejected --yes --reason "Tuning guard is too broad"
|
|
@@ -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/app-config.js
CHANGED
|
@@ -46,7 +46,7 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
46
46
|
tracesEndpoint: null,
|
|
47
47
|
logsEndpoint: null,
|
|
48
48
|
headers: {},
|
|
49
|
-
logLevel: '
|
|
49
|
+
logLevel: 'error',
|
|
50
50
|
disableInstrumentations: [],
|
|
51
51
|
},
|
|
52
52
|
runtime: {
|
|
@@ -102,7 +102,7 @@ const CONFIG_EXPLANATIONS = Object.freeze({
|
|
|
102
102
|
'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
|
|
103
103
|
'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
|
|
104
104
|
'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-api-key from app.key when missing.',
|
|
105
|
-
'config.otel.logLevel': 'OpenTelemetry diagnostic log level:
|
|
105
|
+
'config.otel.logLevel': 'OpenTelemetry diagnostic log level: error, warn, info, debug, or none.',
|
|
106
106
|
'config.otel.disableInstrumentations': 'Optional OTel instrumentation package names to disable.',
|
|
107
107
|
'config.runtime.deploymentEnvironment': 'deployment.environment resource attribute. Set this in the credentials file for production.',
|
|
108
108
|
'config.runtime.noUuid': 'null means auto: true when app.key is present. Set true/false only for advanced routing needs.',
|
package/cli/diagnostics.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
const url = require('url');
|
|
5
7
|
const ui = require('./ui');
|
|
6
8
|
const config = require('./config');
|
|
@@ -37,6 +39,7 @@ function resolvedConfig(options = {}) {
|
|
|
37
39
|
loggingEnabled: appConfig.boolEnv('SECURENOW_LOGGING_ENABLED', true),
|
|
38
40
|
captureBody: appConfig.boolEnv('SECURENOW_CAPTURE_BODY', true),
|
|
39
41
|
captureMultipart: appConfig.boolEnv('SECURENOW_CAPTURE_MULTIPART', true),
|
|
42
|
+
otelLogLevel: (process.env.OTEL_LOG_LEVEL != null ? process.env.OTEL_LOG_LEVEL : appConfig.env('OTEL_LOG_LEVEL')) || 'error',
|
|
40
43
|
firewallEnabled,
|
|
41
44
|
firewallLayers: {
|
|
42
45
|
http: firewallEnabled,
|
|
@@ -47,6 +50,68 @@ function resolvedConfig(options = {}) {
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
function readPackageVersion(packageJsonPath, label) {
|
|
54
|
+
try {
|
|
55
|
+
if (!packageJsonPath || !fs.existsSync(packageJsonPath)) return null;
|
|
56
|
+
const realPath = fs.realpathSync(packageJsonPath);
|
|
57
|
+
const pkg = JSON.parse(fs.readFileSync(realPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
58
|
+
return { label, path: realPath, version: pkg.version || null };
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolvePackageJson(pkgName, paths, label) {
|
|
65
|
+
try {
|
|
66
|
+
let current = path.dirname(require.resolve(pkgName, { paths }));
|
|
67
|
+
while (current && current !== path.dirname(current)) {
|
|
68
|
+
const candidate = path.join(current, 'package.json');
|
|
69
|
+
const pkg = readPackageVersion(candidate, label);
|
|
70
|
+
if (pkg) return pkg;
|
|
71
|
+
current = path.dirname(current);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collectOtelApiPackages() {
|
|
80
|
+
const cwd = process.cwd();
|
|
81
|
+
const packageName = '@opentelemetry/api';
|
|
82
|
+
const candidates = [
|
|
83
|
+
readPackageVersion(path.join(cwd, 'node_modules', '@opentelemetry', 'api', 'package.json'), 'project node_modules'),
|
|
84
|
+
readPackageVersion(path.join(cwd, 'node_modules', 'securenow', 'node_modules', '@opentelemetry', 'api', 'package.json'), 'nested under securenow'),
|
|
85
|
+
resolvePackageJson(packageName, [cwd], 'resolved from project'),
|
|
86
|
+
resolvePackageJson(packageName, [path.resolve(__dirname, '..')], 'resolved from securenow CLI'),
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
|
|
89
|
+
const byPath = new Map();
|
|
90
|
+
for (const item of candidates) {
|
|
91
|
+
if (!byPath.has(item.path)) byPath.set(item.path, { ...item, labels: [item.label] });
|
|
92
|
+
else byPath.get(item.path).labels.push(item.label);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [...byPath.values()].map(({ label, labels, ...rest }) => ({
|
|
96
|
+
...rest,
|
|
97
|
+
label: labels.join(', '),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function otelApiSingletonCheck() {
|
|
102
|
+
const packages = collectOtelApiPackages();
|
|
103
|
+
const versions = [...new Set(packages.map((p) => p.version).filter(Boolean))];
|
|
104
|
+
return {
|
|
105
|
+
name: 'otel-api-singleton',
|
|
106
|
+
ok: versions.length <= 1,
|
|
107
|
+
versions,
|
|
108
|
+
packages,
|
|
109
|
+
...(versions.length > 1 ? {
|
|
110
|
+
error: `Multiple @opentelemetry/api versions detected (${versions.join(', ')}). This can leave tracing on the NoopTracerProvider.`,
|
|
111
|
+
} : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
function maskSecret(value) {
|
|
51
116
|
if (!value) return '';
|
|
52
117
|
const text = String(value);
|
|
@@ -308,6 +373,7 @@ function env(_args, flags) {
|
|
|
308
373
|
firewallApiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
|
|
309
374
|
apiUrl: cfg.apiUrl,
|
|
310
375
|
loggingEnabled: cfg.loggingEnabled,
|
|
376
|
+
otelLogLevel: cfg.otelLogLevel,
|
|
311
377
|
captureBody: cfg.captureBody,
|
|
312
378
|
captureMultipart: cfg.captureMultipart,
|
|
313
379
|
noUuid: appConfig.resolveNoUuid(),
|
|
@@ -326,6 +392,7 @@ function env(_args, flags) {
|
|
|
326
392
|
['Environment', cfg.deploymentEnvironment],
|
|
327
393
|
['Traces endpoint', cfg.tracesEndpoint],
|
|
328
394
|
['Logs endpoint', cfg.logsEndpoint],
|
|
395
|
+
['OTel diagnostics', cfg.otelLogLevel],
|
|
329
396
|
['Logging', cfg.loggingEnabled ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
330
397
|
['Body capture', cfg.captureBody ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
331
398
|
['Multipart capture', cfg.captureMultipart ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
@@ -342,6 +409,8 @@ function env(_args, flags) {
|
|
|
342
409
|
async function doctor(_args, flags) {
|
|
343
410
|
const cfg = resolvedConfig();
|
|
344
411
|
const checks = [];
|
|
412
|
+
const otelApiCheck = otelApiSingletonCheck();
|
|
413
|
+
checks.push(otelApiCheck);
|
|
345
414
|
|
|
346
415
|
const jsonHeaders = { 'Content-Type': 'application/json', ...cfg.headers };
|
|
347
416
|
|
|
@@ -386,6 +455,15 @@ async function doctor(_args, flags) {
|
|
|
386
455
|
}
|
|
387
456
|
|
|
388
457
|
const warnings = [];
|
|
458
|
+
const singletonOkMessage = otelApiCheck.ok && otelApiCheck.packages.length
|
|
459
|
+
? `OpenTelemetry API singleton OK (${otelApiCheck.versions[0] || 'unknown'})`
|
|
460
|
+
: null;
|
|
461
|
+
if (!otelApiCheck.ok) {
|
|
462
|
+
warnings.push(`${otelApiCheck.error} Align the app and SecureNow to one 1.9.x copy, then reinstall.`);
|
|
463
|
+
}
|
|
464
|
+
if (String(cfg.otelLogLevel || '').toLowerCase() === 'none') {
|
|
465
|
+
warnings.push('OpenTelemetry diagnostic log level is `none`; provider registration/export errors are hidden. Set config.otel.logLevel to `error`/`warn`, or temporarily run with OTEL_LOG_LEVEL=debug.');
|
|
466
|
+
}
|
|
389
467
|
if (!cfg.appKey) {
|
|
390
468
|
warnings.push('No app key resolved. Run `npx securenow login` or set app.key in .securenow/credentials.json.');
|
|
391
469
|
}
|
|
@@ -410,6 +488,8 @@ async function doctor(_args, flags) {
|
|
|
410
488
|
for (const w of warnings) ui.warn(w);
|
|
411
489
|
}
|
|
412
490
|
|
|
491
|
+
if (singletonOkMessage) ui.success(singletonOkMessage);
|
|
492
|
+
|
|
413
493
|
console.log('');
|
|
414
494
|
if (ok) ui.success('All checks passed.');
|
|
415
495
|
else ui.error('One or more checks failed. Run with --json for details.');
|
package/cli/human.js
CHANGED
|
@@ -42,6 +42,78 @@ function parseTaskRef(ref) {
|
|
|
42
42
|
return { kind: 'notification', notificationId: text };
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function parseJsonOrList(value) {
|
|
46
|
+
if (value == null || value === true) return [];
|
|
47
|
+
if (Array.isArray(value)) return value.filter(Boolean).map(String);
|
|
48
|
+
const text = String(value).trim();
|
|
49
|
+
if (!text) return [];
|
|
50
|
+
if (text.startsWith('[')) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(text);
|
|
53
|
+
return Array.isArray(parsed) ? parsed.filter(Boolean).map(String) : [];
|
|
54
|
+
} catch {}
|
|
55
|
+
}
|
|
56
|
+
return text.split(/\r?\n|;;/).map((item) => item.trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseCommaList(value) {
|
|
60
|
+
if (value == null || value === true) return [];
|
|
61
|
+
if (Array.isArray(value)) return value.flatMap(parseCommaList);
|
|
62
|
+
const text = String(value).trim();
|
|
63
|
+
if (!text) return [];
|
|
64
|
+
if (text.startsWith('[')) return parseJsonOrList(text);
|
|
65
|
+
return text.split(',').map((item) => item.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function defaultReviewedHistory(task = {}) {
|
|
69
|
+
const history = [];
|
|
70
|
+
if (taskLabel(task)) history.push(`Case: ${taskLabel(task)}`);
|
|
71
|
+
if (task.aiDecision?.label || task.aiDecision?.recommendation) history.push(`AI recommendation: ${task.aiDecision.label || task.aiDecision.recommendation}`);
|
|
72
|
+
if (task.aiDecision?.reason) history.push(`AI reason: ${task.aiDecision.reason}`);
|
|
73
|
+
if (task.aiDecision?.confidence != null) history.push(`AI confidence: ${Math.round(Number(task.aiDecision.confidence))}%`);
|
|
74
|
+
if (task.proofs?.paths?.length) history.push(`Paths: ${formatList(task.proofs.paths, 8)}`);
|
|
75
|
+
if (task.proofs?.methods?.length) history.push(`Methods: ${formatList(task.proofs.methods, 8)}`);
|
|
76
|
+
if (task.proofs?.statusCodes?.length) history.push(`Status codes: ${formatList(task.proofs.statusCodes.map(String), 8)}`);
|
|
77
|
+
if (task.proofs?.userAgents?.length) history.push(`User agents: ${formatList(task.proofs.userAgents, 4)}`);
|
|
78
|
+
if (task.proofs?.traceIds?.length) history.push(`Trace IDs: ${formatList(task.proofs.traceIds, 8)}`);
|
|
79
|
+
return history;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildDecisionReport(task, flags = {}, outcome, fallbackSummary) {
|
|
83
|
+
let report = {};
|
|
84
|
+
if (flags.report) {
|
|
85
|
+
try {
|
|
86
|
+
report = JSON.parse(flags.report);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
ui.error(`Invalid --report JSON: ${err.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const explicitEvidence = parseJsonOrList(flags.evidence);
|
|
94
|
+
const explicitHistory = parseJsonOrList(flags.history);
|
|
95
|
+
const explicitMissingProof = parseJsonOrList(flags['missing-proof']);
|
|
96
|
+
const explicitRecommendations = parseJsonOrList(flags.recommendations);
|
|
97
|
+
const traceIds = parseCommaList(flags['trace-ids']);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
source: 'cli',
|
|
101
|
+
outcome: flags.outcome || report.outcome || outcome || 'other',
|
|
102
|
+
summary: flags.summary || report.summary || fallbackSummary || flags.reason || '',
|
|
103
|
+
reason: flags.reason || report.reason || '',
|
|
104
|
+
evidence: explicitEvidence.length ? explicitEvidence : asArray(report.evidence),
|
|
105
|
+
reviewedHistory: explicitHistory.length ? explicitHistory : (asArray(report.reviewedHistory).length ? asArray(report.reviewedHistory) : defaultReviewedHistory(task)),
|
|
106
|
+
traceIds: traceIds.length ? traceIds : (asArray(report.traceIds).length ? asArray(report.traceIds) : asArray(task?.proofs?.traceIds)),
|
|
107
|
+
paths: asArray(report.paths).length ? asArray(report.paths) : asArray(task?.proofs?.paths),
|
|
108
|
+
methods: asArray(report.methods).length ? asArray(report.methods) : asArray(task?.proofs?.methods),
|
|
109
|
+
statusCodes: asArray(report.statusCodes).length ? asArray(report.statusCodes) : asArray(task?.proofs?.statusCodes).map(String),
|
|
110
|
+
userAgents: asArray(report.userAgents).length ? asArray(report.userAgents) : asArray(task?.proofs?.userAgents),
|
|
111
|
+
missingProof: explicitMissingProof.length ? explicitMissingProof : asArray(report.missingProof),
|
|
112
|
+
recommendations: explicitRecommendations.length ? explicitRecommendations : asArray(report.recommendations),
|
|
113
|
+
rawData: report.rawData || null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
45
117
|
async function fetchQueue(flags = {}) {
|
|
46
118
|
const query = {
|
|
47
119
|
page: flags.page || 1,
|
|
@@ -180,7 +252,7 @@ function printAiReport(ipReport = {}, notification = {}) {
|
|
|
180
252
|
|
|
181
253
|
if (steps.length) {
|
|
182
254
|
console.log('');
|
|
183
|
-
console.log(` ${ui.c.bold('
|
|
255
|
+
console.log(` ${ui.c.bold('Investigation steps')}`);
|
|
184
256
|
const rows = steps.map((step) => [
|
|
185
257
|
step.id,
|
|
186
258
|
step.status || '-',
|
|
@@ -323,12 +395,13 @@ async function action(args, flags) {
|
|
|
323
395
|
|
|
324
396
|
const result = flags.result ? JSON.parse(flags.result) : {};
|
|
325
397
|
if (flags.reason) result.reason = flags.reason;
|
|
398
|
+
const decisionReport = buildDecisionReport(task || { notificationId, actionKey }, flags, flags.outcome || 'case_action', result.summary || result.reason || `Case action ${status}`);
|
|
326
399
|
|
|
327
400
|
const s = ui.spinner('Updating case action');
|
|
328
401
|
try {
|
|
329
402
|
const data = await api.put(
|
|
330
403
|
`/notifications/${encodeURIComponent(notificationId)}/agent-case/actions/${encodeURIComponent(actionKey)}`,
|
|
331
|
-
{ status, result }
|
|
404
|
+
{ status, result, reportSource: 'cli', ips: parseCommaList(flags.ips), decisionReport }
|
|
332
405
|
);
|
|
333
406
|
s.stop('Case action updated');
|
|
334
407
|
if (flags.json) { ui.json(data); return; }
|
|
@@ -358,6 +431,8 @@ async function block(args, flags) {
|
|
|
358
431
|
riskScore: task.aiDecision?.riskScore ?? undefined,
|
|
359
432
|
decisionSource: 'ai_dag',
|
|
360
433
|
note: reason,
|
|
434
|
+
reportSource: 'cli',
|
|
435
|
+
decisionReport: buildDecisionReport(task, flags, 'blocked', reason),
|
|
361
436
|
};
|
|
362
437
|
const data = await api.put(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/status`, body);
|
|
363
438
|
s.stop(`${task.ip} blocked`);
|
|
@@ -391,6 +466,8 @@ async function fp(args, flags) {
|
|
|
391
466
|
applyToExisting: flags['apply-existing'] != null ? flags['apply-existing'] !== 'false' : Boolean(scope.applyToExisting),
|
|
392
467
|
ruleScope: flags['rule-scope'] || scope.ruleScope || 'this_rule',
|
|
393
468
|
aiConfidence: task.aiDecision?.confidence ?? null,
|
|
469
|
+
reportSource: 'cli',
|
|
470
|
+
decisionReport: buildDecisionReport(task, flags, 'false_positive', reason),
|
|
394
471
|
};
|
|
395
472
|
const data = await api.post(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/false-positive`, body);
|
|
396
473
|
s.stop(`${task.ip} marked false positive`);
|
|
@@ -402,6 +479,43 @@ async function fp(args, flags) {
|
|
|
402
479
|
}
|
|
403
480
|
}
|
|
404
481
|
|
|
482
|
+
async function report(args, flags) {
|
|
483
|
+
requireAuth();
|
|
484
|
+
const ref = args[0];
|
|
485
|
+
const { task } = await resolveTask(ref, flags);
|
|
486
|
+
const outcome = flags.outcome || (task.kind === 'case_action' || task.actionKey ? 'case_action' : 'other');
|
|
487
|
+
const summary = flags.summary || flags.reason || `Decision report recorded for ${taskLabel(task)}`;
|
|
488
|
+
|
|
489
|
+
if (!flags.yes && !flags.force) {
|
|
490
|
+
const target = task.ip ? `${task.ip}` : `${task.notificationId}`;
|
|
491
|
+
const ok = await ui.confirm(`Record decision report for ${target}?`);
|
|
492
|
+
if (!ok) return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const s = ui.spinner('Recording decision report');
|
|
496
|
+
try {
|
|
497
|
+
const decisionReport = buildDecisionReport(task, flags, outcome, summary);
|
|
498
|
+
let data;
|
|
499
|
+
if (task.ip) {
|
|
500
|
+
data = await api.post(
|
|
501
|
+
`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}/decision-report`,
|
|
502
|
+
{ reportSource: 'cli', reason: flags.reason || summary, outcome, decisionReport }
|
|
503
|
+
);
|
|
504
|
+
} else {
|
|
505
|
+
data = await api.post(
|
|
506
|
+
`/notifications/${encodeURIComponent(task.notificationId)}/decision-report`,
|
|
507
|
+
{ reportSource: 'cli', reason: flags.reason || summary, outcome, ips: parseCommaList(flags.ips), decisionReport }
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
s.stop('Decision report recorded');
|
|
511
|
+
if (flags.json) ui.json(data);
|
|
512
|
+
else ui.success(`Recorded decision report: ${decisionReport.outcome}`);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
s.fail('Failed to record decision report');
|
|
515
|
+
throw err;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
405
519
|
function mcpPromptText(ref, flags = {}) {
|
|
406
520
|
const rowText = ref ? `Start with human action row/id: ${ref}.` : 'Work the human action queue from most urgent to least urgent.';
|
|
407
521
|
const limit = flags.limit || 10;
|
|
@@ -417,14 +531,15 @@ function mcpPromptText(ref, flags = {}) {
|
|
|
417
531
|
ref
|
|
418
532
|
? '2. Select the requested row number or task id, then call securenow_notifications_get and securenow_human_action_report for its notificationId and IP.'
|
|
419
533
|
: '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
|
|
420
|
-
'3. Read the AI report, finalDecision,
|
|
534
|
+
'3. Read the AI report, finalDecision, investigation steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
421
535
|
'4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
|
|
422
536
|
'5. Decide one outcome: block the IP, mark a scoped false positive, recommend alert-rule tuning, or skip if evidence is ambiguous.',
|
|
423
|
-
'6. For block decisions, call securenow_human_action_block with confirm:true and a
|
|
424
|
-
'7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions,
|
|
537
|
+
'6. For block decisions, call securenow_human_action_block with confirm:true, a precise reason, and a decisionReport with summary/evidence/reviewedHistory/traceIds.',
|
|
538
|
+
'7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, a precise reason, and a decisionReport.',
|
|
425
539
|
'8. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
|
|
426
|
-
'9.
|
|
427
|
-
'10.
|
|
540
|
+
'9. For skipped/ambiguous/rule-tuning-needed rows that should be auditable without changing status, call securenow_human_action_decision_report_add or securenow_human_case_decision_report_add with the missing proof.',
|
|
541
|
+
'10. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
542
|
+
'11. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
|
|
428
543
|
'',
|
|
429
544
|
'Safety:',
|
|
430
545
|
'- Do not call write tools without confirm:true and a reason.',
|
|
@@ -455,6 +570,7 @@ module.exports = {
|
|
|
455
570
|
action,
|
|
456
571
|
block,
|
|
457
572
|
fp,
|
|
573
|
+
report,
|
|
458
574
|
prompt,
|
|
459
575
|
work,
|
|
460
576
|
};
|
package/cli/security.js
CHANGED
|
@@ -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(['
|
|
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,
|
|
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
|
|
309
|
+
confidenceMinimum: number('Minimum SecureNow IPDB confidence score.', { minimum: 0, maximum: 100 }),
|
|
290
310
|
...environmentInput,
|
|
291
311
|
...confirmSchema,
|
|
292
312
|
}, ['appKey', 'confidenceMinimum', 'confirm', 'reason']),
|
|
@@ -455,7 +475,7 @@ const TOOLS = [
|
|
|
455
475
|
{
|
|
456
476
|
name: 'securenow_human_action_report',
|
|
457
477
|
title: 'Get Human Action Report',
|
|
458
|
-
description: 'Fetch the full IP investigation report,
|
|
478
|
+
description: 'Fetch the full IP investigation report, investigation steps, proofs, metadata, and AI decision for one human action row.',
|
|
459
479
|
scope: 'notifications:read',
|
|
460
480
|
readOnly: true,
|
|
461
481
|
method: 'GET',
|
|
@@ -469,7 +489,7 @@ const TOOLS = [
|
|
|
469
489
|
{
|
|
470
490
|
name: 'securenow_human_action_block',
|
|
471
491
|
title: 'Approve AI Block Recommendation',
|
|
472
|
-
description: 'Approve the AI-prepared block decision for an IP. Write action; requires confirmation.',
|
|
492
|
+
description: 'Approve the AI-prepared block decision for an IP and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
|
|
473
493
|
scope: 'notifications:write',
|
|
474
494
|
readOnly: false,
|
|
475
495
|
destructive: true,
|
|
@@ -477,29 +497,50 @@ const TOOLS = [
|
|
|
477
497
|
method: 'PUT',
|
|
478
498
|
endpoint: '/notifications/:notificationId/ips/:ip/status',
|
|
479
499
|
pathParams: ['notificationId', 'ip'],
|
|
480
|
-
fixedBody: { status: 'blocked', decisionSource: 'ai_dag' },
|
|
500
|
+
fixedBody: { status: 'blocked', decisionSource: 'ai_dag', reportSource: 'mcp' },
|
|
481
501
|
reasonAsNote: true,
|
|
482
|
-
bodyFields: ['note', 'verdict', 'riskScore'],
|
|
502
|
+
bodyFields: ['note', 'verdict', 'riskScore', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
483
503
|
inputSchema: objectSchema({
|
|
484
504
|
notificationId: string('Notification id from the human action row.'),
|
|
485
505
|
ip: string('IP address to block.'),
|
|
486
506
|
note: string('Audit note explaining why the AI block was approved.'),
|
|
487
507
|
verdict: string('Optional final verdict text.'),
|
|
488
508
|
riskScore: number('Optional risk score.', { minimum: 0, maximum: 100 }),
|
|
509
|
+
...decisionReportInput,
|
|
510
|
+
...confirmSchema,
|
|
511
|
+
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'securenow_human_action_decision_report_add',
|
|
515
|
+
title: 'Record Human Action Decision Report',
|
|
516
|
+
description: 'Attach a structured analyst/MCP decision report to one Requires Human IP row without changing its status. Use for skipped, ambiguous, rule-tuning-needed, or already-handled rows. Write action; requires confirmation.',
|
|
517
|
+
scope: 'notifications:write',
|
|
518
|
+
readOnly: false,
|
|
519
|
+
confirm: true,
|
|
520
|
+
method: 'POST',
|
|
521
|
+
endpoint: '/notifications/:notificationId/ips/:ip/decision-report',
|
|
522
|
+
pathParams: ['notificationId', 'ip'],
|
|
523
|
+
fixedBody: { reportSource: 'mcp' },
|
|
524
|
+
bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
525
|
+
inputSchema: objectSchema({
|
|
526
|
+
notificationId: string('Notification id from the human action row.'),
|
|
527
|
+
ip: string('IP address for the report.'),
|
|
528
|
+
...decisionReportInput,
|
|
489
529
|
...confirmSchema,
|
|
490
530
|
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
491
531
|
},
|
|
492
532
|
{
|
|
493
533
|
name: 'securenow_human_action_false_positive',
|
|
494
534
|
title: 'Mark Human Action False Positive',
|
|
495
|
-
description: 'Mark an AI-prepared human action as a scoped false positive. Write action; requires confirmation.',
|
|
535
|
+
description: 'Mark an AI-prepared human action as a scoped false positive and optionally attach a structured decision report to the IP history. Write action; requires confirmation.',
|
|
496
536
|
scope: 'notifications:write',
|
|
497
537
|
readOnly: false,
|
|
498
538
|
confirm: true,
|
|
499
539
|
method: 'POST',
|
|
500
540
|
endpoint: '/notifications/:notificationId/ips/:ip/false-positive',
|
|
501
541
|
pathParams: ['notificationId', 'ip'],
|
|
502
|
-
|
|
542
|
+
fixedBody: { reportSource: 'mcp' },
|
|
543
|
+
bodyFields: ['reason', 'conditions', 'matchMode', 'createExclusion', 'applyToExisting', 'ruleScope', 'targetRuleIds', 'aiConfidence', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
503
544
|
inputSchema: objectSchema({
|
|
504
545
|
notificationId: string('Notification id from the human action row.'),
|
|
505
546
|
ip: string('IP address to mark as false positive.'),
|
|
@@ -510,6 +551,7 @@ const TOOLS = [
|
|
|
510
551
|
ruleScope: string('Scope: this_rule, specific_rules, all_existing, or any_rule.'),
|
|
511
552
|
targetRuleIds: arrayOfStrings('Specific rule ids when ruleScope is specific_rules.'),
|
|
512
553
|
aiConfidence: number('AI confidence for audit metadata.', { minimum: 0, maximum: 100 }),
|
|
554
|
+
...decisionReportInput,
|
|
513
555
|
...confirmSchema,
|
|
514
556
|
}, ['notificationId', 'ip', 'confirm', 'reason']),
|
|
515
557
|
},
|
|
@@ -523,16 +565,36 @@ const TOOLS = [
|
|
|
523
565
|
method: 'PUT',
|
|
524
566
|
endpoint: '/notifications/:notificationId/agent-case/actions/:actionKey',
|
|
525
567
|
pathParams: ['notificationId', 'actionKey'],
|
|
526
|
-
|
|
568
|
+
fixedBody: { reportSource: 'mcp' },
|
|
569
|
+
bodyFields: ['status', 'result', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
527
570
|
reasonInResult: true,
|
|
528
571
|
inputSchema: objectSchema({
|
|
529
572
|
notificationId: string('Notification id from the case-action row.'),
|
|
530
573
|
actionKey: string('Proposed action key from the row, for example tune_rule:...'),
|
|
531
574
|
status: string('New status: proposed, approved, rejected, executed, or failed.'),
|
|
532
575
|
result: { type: 'object', additionalProperties: true, description: 'Optional structured result/audit details.' },
|
|
576
|
+
...decisionReportInput,
|
|
533
577
|
...confirmSchema,
|
|
534
578
|
}, ['notificationId', 'actionKey', 'status', 'confirm', 'reason']),
|
|
535
579
|
},
|
|
580
|
+
{
|
|
581
|
+
name: 'securenow_human_case_decision_report_add',
|
|
582
|
+
title: 'Record Case Decision Report',
|
|
583
|
+
description: 'Attach a structured analyst/MCP decision report to a notification/case without changing IP status or proposed action state. Write action; requires confirmation.',
|
|
584
|
+
scope: 'notifications:write',
|
|
585
|
+
readOnly: false,
|
|
586
|
+
confirm: true,
|
|
587
|
+
method: 'POST',
|
|
588
|
+
endpoint: '/notifications/:notificationId/decision-report',
|
|
589
|
+
pathParams: ['notificationId'],
|
|
590
|
+
fixedBody: { reportSource: 'mcp' },
|
|
591
|
+
bodyFields: ['reason', 'decisionReport', 'decisionSummary', 'outcome', 'evidence', 'reviewedHistory', 'traceIds', 'paths', 'ips', 'methods', 'statusCodes', 'userAgents', 'missingProof', 'recommendations'],
|
|
592
|
+
inputSchema: objectSchema({
|
|
593
|
+
notificationId: string('Notification id from the case-action row.'),
|
|
594
|
+
...decisionReportInput,
|
|
595
|
+
...confirmSchema,
|
|
596
|
+
}, ['notificationId', 'confirm', 'reason']),
|
|
597
|
+
},
|
|
536
598
|
{
|
|
537
599
|
name: 'securenow_ip_lookup',
|
|
538
600
|
title: 'IP Intelligence Lookup',
|
|
@@ -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,
|
|
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
|
|
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/
|
|
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
|
|
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.
|
|
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",
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
"SKILL-API.md"
|
|
142
142
|
],
|
|
143
143
|
"dependencies": {
|
|
144
|
-
"@opentelemetry/api": "1.9.1",
|
|
144
|
+
"@opentelemetry/api": ">=1.9.0 <1.10.0",
|
|
145
145
|
"@opentelemetry/api-logs": "0.218.0",
|
|
146
146
|
"@opentelemetry/auto-instrumentations-node": "0.76.0",
|
|
147
147
|
"@opentelemetry/core": "2.7.1",
|
package/tracing.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* OTEL_EXPORTER_OTLP_HEADERS="k=v,k2=v2"
|
|
22
22
|
* SECURENOW_DISABLE_INSTRUMENTATIONS="pkg1,pkg2"
|
|
23
23
|
* SECURENOW_CAPTURE_MULTIPART=1 # capture multipart/form-data fields & file metadata (streaming, no file content buffered)
|
|
24
|
-
* OTEL_LOG_LEVEL=info|debug
|
|
24
|
+
* OTEL_LOG_LEVEL=error|warn|info|debug|none
|
|
25
25
|
* SECURENOW_TEST_SPAN=1
|
|
26
26
|
*
|
|
27
27
|
* Safety:
|
|
@@ -264,7 +264,7 @@ function collectMultipartMeta(request, contentType, sensitiveFields, maxTextFiel
|
|
|
264
264
|
})();
|
|
265
265
|
|
|
266
266
|
// -------- diagnostics --------
|
|
267
|
-
const diagLevel = (env('OTEL_LOG_LEVEL') || '').toLowerCase();
|
|
267
|
+
const diagLevel = ((process.env.OTEL_LOG_LEVEL != null ? process.env.OTEL_LOG_LEVEL : env('OTEL_LOG_LEVEL')) || '').toLowerCase();
|
|
268
268
|
(() => {
|
|
269
269
|
const level = diagLevel === 'debug' ? DiagLogLevel.DEBUG :
|
|
270
270
|
diagLevel === 'info' ? DiagLogLevel.INFO :
|