securenow 8.21.0 → 8.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL-CLI.md +5 -0
- package/cli/protect.js +64 -2
- package/cli/security.js +72 -0
- package/cli.js +5 -3
- package/mcp/catalog.js +31 -2
- package/package.json +1 -1
package/SKILL-CLI.md
CHANGED
|
@@ -398,6 +398,7 @@ securenow alerts rules change <id> "raise the threshold to 20" # rewrite a rul
|
|
|
398
398
|
securenow alerts rules runs <id> [--since 1h] # what each run saw and why it did or didn't notify (+ next scheduled run)
|
|
399
399
|
securenow alerts rules run <id> # run a periodic rule now and print the run (--no-wait to just queue it)
|
|
400
400
|
securenow alerts rules why <id> --ip <ip> [--since 1h] # why the rule did or didn't alert on one address: verdict + findings
|
|
401
|
+
securenow alerts rules quality <id> # was the rule right to fire? 14-day precision, alerts labeled wrong/unclear and why, what detection learning changed
|
|
401
402
|
securenow alerts rules dry-run-query <id> --sql @candidate.sql --app <key> --wait # candidate test against a specific host rule
|
|
402
403
|
securenow alerts rules tune-query <id> --sql @candidate.sql --reason "Preserve exploit detector, remove benign broad match" --apply-globally --yes
|
|
403
404
|
securenow alerts rules exclusions <id> list # embedded rule exclusions
|
|
@@ -487,10 +488,14 @@ securenow protect enable --app <key> # turn it on (monitor mode)
|
|
|
487
488
|
securenow protect block --app <key> # a person turns blocking on; refused unless the app sees real client IPs
|
|
488
489
|
securenow protect monitor --app <key> # back to alerts only
|
|
489
490
|
securenow protect disable --app <key> # off; stays off until someone turns it on
|
|
491
|
+
securenow protect learn --app <key> # detection learning now (it also runs hourly): label alerts, measure precision, learn safe exclusions
|
|
492
|
+
securenow protect status --rules # adds each rule's 14-day precision (right / right + wrong)
|
|
490
493
|
```
|
|
491
494
|
|
|
492
495
|
Only a person signed in (`securenow admin login`) can turn blocking on; API keys, MCP connectors and the agent get a 403. Blocking is refused when more than 20% of the app's traffic reaches it through a proxy the SDK doesn't trust: add the proxy to `config.networking.trustedProxies` (IPs, IPv4 ranges or `"cloudflare"`). Tune a noisy starter rule like any rule (exclusions, `alerts rules update`); pack updates never overwrite a rule a person edited.
|
|
493
496
|
|
|
497
|
+
**Detection learning** keeps the starter rules precise per app. Every alert gets a label: was the rule right to fire? A blocked address counts as right, a person's false-positive call as wrong, and an LLM judge (accounts with AI access) labels the rest from the requests and the address's last day of traffic; a failed one-off probe (`/.env` answered 404) is closed as right but not worth action. Precision = right / (right + wrong) over 14 days, target 95%. A rule below it with 3+ wrong alerts gets one exclusion for the legitimate traffic, applied only when it matches the wrong alerts and none of the right ones, can't be pinned to an IP, and still lets every reference attack through even when injected into the excused request; otherwise the rule is flagged for a person. A block-ready rule under 95% (10+ labeled alerts) keeps the app from being ready to block. MCP: `securenow_alert_rule_quality`, `securenow_protection_learn`.
|
|
498
|
+
|
|
494
499
|
### Blocklist — Block Malicious IPs
|
|
495
500
|
|
|
496
501
|
```bash
|
package/cli/protect.js
CHANGED
|
@@ -31,13 +31,27 @@ function modeLabel(app) {
|
|
|
31
31
|
return ui.c.yellow(app.state === 'unset' ? 'NOT YET ON' : 'OFF');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Precision: of the alerts labeled right or wrong (detection learning), the share where the rule was right.
|
|
35
|
+
const PRECISION_TARGET = 0.95;
|
|
36
|
+
|
|
37
|
+
function precisionCell(quality) {
|
|
38
|
+
if (!quality || quality.precision == null) return ui.c.dim('-');
|
|
39
|
+
const decided = quality.decided ?? (quality.correct || 0) + (quality.wrong || 0);
|
|
40
|
+
const text = `${(quality.precision * 100).toFixed(quality.precision === 1 ? 0 : 1)}% (${quality.correct}/${decided})`;
|
|
41
|
+
return quality.precision >= PRECISION_TARGET ? ui.c.green(text) : ui.c.yellow(text);
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
function printApp(app, { verbose }) {
|
|
35
45
|
console.log('');
|
|
36
46
|
console.log(` ${ui.c.bold(app.appName)} ${ui.c.dim(app.appKey)} ${modeLabel(app)}`);
|
|
37
47
|
const summary = app.summary || {};
|
|
48
|
+
const quality = summary.quality || {};
|
|
38
49
|
ui.keyValue([
|
|
39
50
|
['Rules', `${summary.active || 0} active of ${summary.rules || 0} (${summary.blockReady || 0} can block, ${summary.blocking || 0} blocking)`],
|
|
40
51
|
['Alerts, last 7 days', `${summary.alerts7d || 0} (${summary.falsePositives7d || 0} marked false positive)`],
|
|
52
|
+
['Precision, last 14 days', quality.precision == null
|
|
53
|
+
? ui.c.dim('not measured yet: no labeled alerts')
|
|
54
|
+
: `${precisionCell(quality)} right${quality.wrong ? `, ${quality.wrong} wrong` : ''}${quality.unclear ? `, ${quality.unclear} unclear` : ''}`],
|
|
41
55
|
['Pack version', `${app.packVersion || '-'} (latest ${app.latestPackVersion})${summary.customized ? `, ${summary.customized} rule(s) edited by a person` : ''}${summary.missing ? `, ${summary.missing} missing` : ''}`],
|
|
42
56
|
]);
|
|
43
57
|
if (app.mode === 'monitor') {
|
|
@@ -50,16 +64,18 @@ function printApp(app, { verbose }) {
|
|
|
50
64
|
}
|
|
51
65
|
if (verbose) {
|
|
52
66
|
ui.table(
|
|
53
|
-
['Rule', 'Kind', 'Mode', 'Status', 'Alerts 7d', 'Blocks'],
|
|
67
|
+
['Rule', 'Kind', 'Mode', 'Status', 'Alerts 7d', 'Precision 14d', 'Blocks'],
|
|
54
68
|
(app.rules || []).map((rule) => [
|
|
55
69
|
ui.truncate(rule.name, 52),
|
|
56
70
|
rule.kind,
|
|
57
71
|
rule.present ? rule.mode : '-',
|
|
58
72
|
rule.present ? `${rule.status}${rule.customized ? ' (edited)' : ''}` : ui.c.dim('missing'),
|
|
59
73
|
String(rule.alerts7d || 0),
|
|
74
|
+
rule.present ? precisionCell(rule.quality) : '-',
|
|
60
75
|
rule.blocking ? ui.c.green('yes') : rule.blockReady ? ui.c.dim('ready') : ui.c.dim('alerts only'),
|
|
61
76
|
])
|
|
62
77
|
);
|
|
78
|
+
console.log(` ${ui.c.dim('A rule\'s labeled alerts and what detection learning changed: securenow alerts rules quality <rule-id> (ids: --json)')}`);
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
|
|
@@ -134,4 +150,50 @@ async function monitor(args, flags) {
|
|
|
134
150
|
return change('/protection/monitor', flags, { verb: 'Switching to monitor mode for' });
|
|
135
151
|
}
|
|
136
152
|
|
|
137
|
-
|
|
153
|
+
const LEARNED = {
|
|
154
|
+
exclusion_added: (item) => `${item.ruleKey} on ${item.appKey}: learned an exclusion for legitimate traffic; precision on the same alerts ${item.after && item.after.precision != null ? `${(item.after.precision * 100).toFixed(1)}%` : 'recomputed next run'}`,
|
|
155
|
+
needs_person: (item) => `${item.ruleKey} on ${item.appKey}: below ${PRECISION_TARGET * 100}% and no exclusion passed the safety checks; a person should tune it${item.refusals && item.refusals[0] ? ` (${ui.truncate(item.refusals[0], 120)})` : ''}`,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Detection learning now (it also runs every hour): label the starter rules' alerts (was the rule right?), measure
|
|
159
|
+
// each rule's precision, and learn exclusions for legitimate traffic that pass the safety checks.
|
|
160
|
+
async function learn(args, flags) {
|
|
161
|
+
requireAuth();
|
|
162
|
+
const appKey = flags.app ? String(flags.app) : (flags.all ? null : resolveAppKey(flags));
|
|
163
|
+
const s = ui.spinner(appKey ? `Running detection learning for ${appKey}` : 'Running detection learning');
|
|
164
|
+
let data;
|
|
165
|
+
try {
|
|
166
|
+
data = await api.post('/protection/learn', appKey ? { app: appKey } : {});
|
|
167
|
+
s.stop(data.message || 'Done');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
s.fail('Detection learning failed');
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
if (flags.json) { ui.json(data); return; }
|
|
173
|
+
const rules = (data.quality || []).filter((rule) => rule.labeled || rule.unclear || rule.unlabeled || rule.superseded);
|
|
174
|
+
console.log('');
|
|
175
|
+
if (rules.length === 0) {
|
|
176
|
+
ui.info('No starter-rule alerts in the last 14 days, so there is nothing to measure yet.');
|
|
177
|
+
} else {
|
|
178
|
+
ui.table(
|
|
179
|
+
['Rule', 'App', 'Precision', 'Right', 'Wrong', 'Unclear', 'Not labeled', 'Fixed'],
|
|
180
|
+
rules.map((rule) => [
|
|
181
|
+
rule.ruleKey,
|
|
182
|
+
ui.truncate(rule.appKey || '-', 12),
|
|
183
|
+
precisionCell(rule),
|
|
184
|
+
String(rule.correct || 0),
|
|
185
|
+
String(rule.wrong || 0),
|
|
186
|
+
String(rule.unclear || 0),
|
|
187
|
+
String(rule.unlabeled || 0),
|
|
188
|
+
String(rule.fixedWrong || 0),
|
|
189
|
+
])
|
|
190
|
+
);
|
|
191
|
+
console.log(` ${ui.c.dim('Precision counts the alerts the rule as it is now would still raise; Fixed = wrong alerts a rule change or exclusion no longer raises.')}`);
|
|
192
|
+
}
|
|
193
|
+
for (const item of data.learned || []) {
|
|
194
|
+
console.log(` ${item.kind === 'exclusion_added' ? ui.c.green('learned') : ui.c.yellow('needs you')} ${(LEARNED[item.kind] || ((x) => `${x.ruleKey}: ${x.kind}`))(item)}`);
|
|
195
|
+
}
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { status, enable, disable, block, monitor, learn };
|
package/cli/security.js
CHANGED
|
@@ -106,6 +106,9 @@ async function alertRulesRoute(args, flags) {
|
|
|
106
106
|
if (sub === 'why' || sub === 'explain') {
|
|
107
107
|
return alertRuleWhy(args.slice(1), flags);
|
|
108
108
|
}
|
|
109
|
+
if (sub === 'quality' || sub === 'precision') {
|
|
110
|
+
return alertRuleQuality(args.slice(1), flags);
|
|
111
|
+
}
|
|
109
112
|
if (sub === 'promote') {
|
|
110
113
|
return alertRuleSetMode(args.slice(1), flags, 'prod');
|
|
111
114
|
}
|
|
@@ -1322,6 +1325,75 @@ async function alertRuleWhy(args, flags) {
|
|
|
1322
1325
|
console.log('');
|
|
1323
1326
|
}
|
|
1324
1327
|
|
|
1328
|
+
// Was the rule right to fire? Its precision over the last 14 days from the labeled alerts, the ones labeled wrong
|
|
1329
|
+
// or unclear and why, and what detection learning changed.
|
|
1330
|
+
async function alertRuleQuality(args, flags) {
|
|
1331
|
+
requireAuth();
|
|
1332
|
+
const id = args[0];
|
|
1333
|
+
if (!id) {
|
|
1334
|
+
ui.error('Usage: securenow alerts rules quality <rule-id> [--json]');
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
const s = ui.spinner('Reading the rule\'s labeled alerts');
|
|
1338
|
+
let data;
|
|
1339
|
+
try {
|
|
1340
|
+
data = await api.get(`/alert-rules/${id}/quality`);
|
|
1341
|
+
s.stop('');
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
s.fail('Failed to read the rule\'s quality');
|
|
1344
|
+
throw err;
|
|
1345
|
+
}
|
|
1346
|
+
if (flags.json) { ui.json(data); return; }
|
|
1347
|
+
const q = data.quality || {};
|
|
1348
|
+
const target = data.target || 0.95;
|
|
1349
|
+
const pct = (value) => `${(value * 100).toFixed(value === 1 ? 0 : 1)}%`;
|
|
1350
|
+
console.log('');
|
|
1351
|
+
ui.heading(data.rule?.name || 'Rule quality');
|
|
1352
|
+
ui.keyValue([
|
|
1353
|
+
[`Precision, last ${data.windowDays || 14} days`, q.precision == null
|
|
1354
|
+
? ui.c.dim('not measured yet: no alert labeled right or wrong')
|
|
1355
|
+
: (q.precision >= target ? ui.c.green : ui.c.yellow)(`${pct(q.precision)} (target ${pct(target)})`)],
|
|
1356
|
+
['Right', `${q.correct || 0} (${q.actionable || 0} worth acting on)`],
|
|
1357
|
+
['Wrong', String(q.wrong || 0)],
|
|
1358
|
+
['Unclear', String(q.unclear || 0)],
|
|
1359
|
+
['Not labeled yet', String(q.unlabeled || 0)],
|
|
1360
|
+
['No longer raised', `${q.superseded || 0}: ${q.fixedWrong || 0} wrong alert(s) fixed by a rule change or exclusion, ${q.droppedCorrect || 0} correct one(s) dropped`],
|
|
1361
|
+
['Never reached anyone', `${q.dismissed || 0} (closed by an exclusion or the pre-filter)`],
|
|
1362
|
+
]);
|
|
1363
|
+
const rowsTable = (title, rows) => {
|
|
1364
|
+
if (!rows || rows.length === 0) return;
|
|
1365
|
+
console.log('');
|
|
1366
|
+
console.log(` ${ui.c.bold(title)}`);
|
|
1367
|
+
ui.table(['When (UTC)', 'Address', 'Request', 'Answer', 'By', 'Why'], rows.map((row) => [
|
|
1368
|
+
new Date(row.at).toISOString().replace('T', ' ').slice(0, 16),
|
|
1369
|
+
row.ip,
|
|
1370
|
+
ui.truncate((row.paths || [])[0] || '-', 40),
|
|
1371
|
+
(row.answers || []).join(',') || '-',
|
|
1372
|
+
row.labeledBy || '-',
|
|
1373
|
+
ui.truncate(row.benignPattern || row.reason || '', 60),
|
|
1374
|
+
]));
|
|
1375
|
+
};
|
|
1376
|
+
rowsTable('Labeled wrong', data.wrong);
|
|
1377
|
+
rowsTable('Unclear', data.unclear);
|
|
1378
|
+
rowsTable('Wrong alerts the rule no longer raises (fixed)', data.fixed);
|
|
1379
|
+
rowsTable('Correct alerts the rule no longer raises (check the change didn\'t lose real attacks)', data.dropped);
|
|
1380
|
+
if ((data.learning || []).length) {
|
|
1381
|
+
console.log('');
|
|
1382
|
+
console.log(` ${ui.c.bold('Detection learning')}`);
|
|
1383
|
+
for (const action of data.learning) {
|
|
1384
|
+
const when = new Date(action.at).toISOString().slice(0, 10);
|
|
1385
|
+
if (action.kind === 'exclusion_added') {
|
|
1386
|
+
console.log(` ${when} ${ui.c.green('exclusion learned')} ${pct(action.before?.precision ?? 0)} → ${pct(action.after?.precision ?? 0)} on the same alerts${action.exclusionId ? ui.c.dim(` (exclusion ${action.exclusionId})`) : ''}`);
|
|
1387
|
+
} else {
|
|
1388
|
+
console.log(` ${when} ${ui.c.yellow('needs a person')} ${ui.truncate(action.reason || '', 110)}`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
console.log('');
|
|
1393
|
+
console.log(ui.c.dim(` Exclusions on this rule: securenow alerts rules exclusions ${id} list`));
|
|
1394
|
+
console.log('');
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1325
1397
|
// An agent hands the promotion of its test-mode rule to a person: the request waits on the rules
|
|
1326
1398
|
// page and in `securenow human promotions` until someone approves or denies it.
|
|
1327
1399
|
async function alertRuleRequestPromotion(args, flags) {
|
package/cli.js
CHANGED
|
@@ -359,12 +359,13 @@ const COMMANDS = {
|
|
|
359
359
|
sub: {
|
|
360
360
|
rules: {
|
|
361
361
|
desc: 'Create (from English or SQL), list, show, update, duplicate, promote/demote, set-sql, validate, delete, test, run, explain, or tune alert rules',
|
|
362
|
-
usage: 'securenow alerts rules <list|from-text|create|show|update|duplicate|promote|demote|set-sql|validate|delete|test|run|runs|why|dry-run-query|tune-query|exclusions|versions|version|rollback> [options]\n'
|
|
362
|
+
usage: 'securenow alerts rules <list|from-text|create|show|update|duplicate|promote|demote|set-sql|validate|delete|test|run|runs|why|quality|dry-run-query|tune-query|exclusions|versions|version|rollback> [options]\n'
|
|
363
363
|
+ ' securenow alerts rules from-text "<what to catch, in English>" --app <key> [--env <environment>] [--instant|--periodic] [--yes|--draft-only]\n'
|
|
364
364
|
+ ' securenow alerts rules change <id> "<what should change>" [--yes|--draft-only] rewrite a rule from English, checked, applied as a new version\n'
|
|
365
365
|
+ ' securenow alerts rules runs <id> [--limit 20] [--since 1h] what each run saw and why it did or didn\'t notify\n'
|
|
366
366
|
+ ' securenow alerts rules run <id> [--no-wait] run a periodic rule now\n'
|
|
367
367
|
+ ' securenow alerts rules why <id> --ip <ip> [--since 1h] why the rule did or didn\'t alert on one address\n'
|
|
368
|
+
+ ' securenow alerts rules quality <id> was the rule right? precision, wrong alerts, what was learned\n'
|
|
368
369
|
+ ' securenow alerts rules request-promotion <id> --reason "..." ask a person to promote a test-mode rule (for agents)',
|
|
369
370
|
flags: {
|
|
370
371
|
json: 'Output as JSON',
|
|
@@ -458,14 +459,15 @@ const COMMANDS = {
|
|
|
458
459
|
},
|
|
459
460
|
protect: {
|
|
460
461
|
desc: 'Starter protection per app: the rules every app gets, monitor or block mode',
|
|
461
|
-
usage: 'securenow protect <status|enable|disable|block|monitor> [--app <key>] [--json]',
|
|
462
|
-
flags: { app: 'App key (defaults to logged-in app)', all: 'With status: every app', rules: 'With status: list each rule', yes: 'Skip the confirmation (block, disable)', json: 'Output as JSON' },
|
|
462
|
+
usage: 'securenow protect <status|enable|disable|block|monitor|learn> [--app <key>] [--json]',
|
|
463
|
+
flags: { app: 'App key (defaults to logged-in app)', all: 'With status or learn: every app', rules: 'With status: list each rule', yes: 'Skip the confirmation (block, disable)', json: 'Output as JSON' },
|
|
463
464
|
sub: {
|
|
464
465
|
status: { desc: 'Each app\'s starter protection: mode, rules, alerts and whether it is ready to block', usage: 'securenow protect status [--app <key> | --all] [--rules] [--json]', run: (a, f) => require('./cli/protect').status(a, f) },
|
|
465
466
|
enable: { desc: 'Turn starter protection on for an app (monitor mode: rules alert, nothing is blocked)', usage: 'securenow protect enable [--app <key>]', run: (a, f) => require('./cli/protect').enable(a, f) },
|
|
466
467
|
disable: { desc: 'Turn starter protection off for an app; it stays off until someone turns it on', usage: 'securenow protect disable [--app <key>] [--yes]', run: (a, f) => require('./cli/protect').disable(a, f) },
|
|
467
468
|
block: { desc: 'Turn blocking on (a person signed in): block-ready rules block exploit signatures for 6 hours', usage: 'securenow protect block [--app <key>] [--yes]', run: (a, f) => require('./cli/protect').block(a, f) },
|
|
468
469
|
monitor: { desc: 'Back to monitor mode: the starter rules alert, nothing is blocked by them', usage: 'securenow protect monitor [--app <key>]', run: (a, f) => require('./cli/protect').monitor(a, f) },
|
|
470
|
+
learn: { desc: 'Detection learning now (it also runs hourly): label the starter rules\' alerts, measure precision, learn safe exclusions', usage: 'securenow protect learn [--app <key> | --all] [--json]', run: (a, f) => require('./cli/protect').learn(a, f) },
|
|
469
471
|
},
|
|
470
472
|
defaultSub: 'status',
|
|
471
473
|
},
|
package/mcp/catalog.js
CHANGED
|
@@ -1345,6 +1345,34 @@ const TOOLS = [
|
|
|
1345
1345
|
...confirmSchema,
|
|
1346
1346
|
}, ['app', 'confirm', 'reason']),
|
|
1347
1347
|
},
|
|
1348
|
+
{
|
|
1349
|
+
name: 'securenow_alert_rule_quality',
|
|
1350
|
+
title: 'Alert Rule Quality (Was It Right to Fire?)',
|
|
1351
|
+
description: 'How often an alert rule is right, from its labeled alerts of the last 14 days: correct (an attack, probe or scan, even failed or small), wrong (the rule mistook legitimate use of the app for an attack), unclear; precision = correct / (correct + wrong) over the alerts the rule as it is now would still raise (each alert is replayed through the current conditions and exclusions), target 95%. fixed lists wrong alerts a rule change or exclusion no longer raises; dropped lists correct ones it no longer raises (lost detections). Lists the wrong and unclear alerts with the reason and the legitimate pattern, a sample of correct ones, and what detection learning did: an exclusion it learned for legitimate traffic (with before/after precision) or a "needs a person" analysis when no safe fix passed its safety gate.',
|
|
1352
|
+
scope: 'alerts:read',
|
|
1353
|
+
readOnly: true,
|
|
1354
|
+
method: 'GET',
|
|
1355
|
+
endpoint: '/alert-rules/:id/quality',
|
|
1356
|
+
pathParams: ['id'],
|
|
1357
|
+
inputSchema: objectSchema({
|
|
1358
|
+
id: string('Alert rule id.'),
|
|
1359
|
+
}, ['id']),
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: 'securenow_protection_learn',
|
|
1363
|
+
title: 'Run Detection Learning Now',
|
|
1364
|
+
description: 'Run detection learning for the account now (it also runs every hour): label the base rules\' recent alerts (was the rule right?), measure each rule\'s precision per app, and learn a safe exclusion for a rule below 95% that keeps mistaking an app\'s legitimate traffic for an attack. A learned exclusion never names an IP and must keep every correct alert and every known attack firing. An account where detection learning isn\'t on yet gets 409 not_rolled_out. Write action; requires confirmation.',
|
|
1365
|
+
scope: 'alerts:write',
|
|
1366
|
+
readOnly: false,
|
|
1367
|
+
confirm: true,
|
|
1368
|
+
method: 'POST',
|
|
1369
|
+
endpoint: '/protection/learn',
|
|
1370
|
+
bodyFields: ['app'],
|
|
1371
|
+
inputSchema: objectSchema({
|
|
1372
|
+
app: string('Optional application key; every app when left out.'),
|
|
1373
|
+
...confirmSchema,
|
|
1374
|
+
}, ['confirm', 'reason']),
|
|
1375
|
+
},
|
|
1348
1376
|
{
|
|
1349
1377
|
name: 'securenow_protection_disable',
|
|
1350
1378
|
title: 'Turn Starter Protection Off',
|
|
@@ -2699,7 +2727,7 @@ const AGENT_PATROL_RUNBOOK = [
|
|
|
2699
2727
|
'B. Requires Human: securenow_human_actions_grouped_list, largest groups first (rows you already decided are hidden). Each group lists up to 12 rows; verify and decide 2 to 4 rows of a group, then move to the next group. For each IP row:',
|
|
2700
2728
|
' - verify the IP;',
|
|
2701
2729
|
' - verdict malicious and blockAllowed: securenow_agent_block_ip with ip, verificationId, notificationId, a reason naming the 2 or 3 decisive signals, confirm true;',
|
|
2702
|
-
' - verdict benign or never_block:
|
|
2730
|
+
' - verdict benign or never_block: a false positive means the rule mistook LEGITIMATE use of the app for an attack. A real probe, scan or attack attempt that is just too small to block (one failed request for /.env, a scanner fetching a page) is not a false positive: the rule was right, so close it with securenow_human_action_close_no_write, outcome "skipped". When the rule plainly misfired on legitimate traffic, securenow_human_action_false_positive with the verificationId, on this rule only (no ruleScope, no applyToExisting). On a test-mode rule that will keep misfiring on this IP, add createExclusion with at most a path condition: the server pins the exclusion to this IP and it expires within 7 days. On a prod rule, never createExclusion. Otherwise securenow_human_action_close_no_write with outcome "skipped" (or "other"), the verificationId and a one-line summary of the evidence. An IP the org has blocked stays blocked: lifting a block is for a person;',
|
|
2703
2731
|
' - verdict suspicious: attack-shaped traffic below the block threshold, which a person decides. Do not block. Record your read with securenow_human_action_close_no_write (outcome "ambiguous", the verificationId, missingProof saying what would decide it): the server leaves it on the row as your suggestion, keeps the row for a person and hides it from your lists for a day. If a person should look now (a real attempt that succeeded, a signed-in account under attack), say so in missingProof.',
|
|
2704
2732
|
' - verdict insufficient_data (nothing from this IP to confirm anything): do not block. securenow_human_action_close_no_write with outcome "ambiguous", the verificationId, and missingProof saying what would decide it.',
|
|
2705
2733
|
' - Rule-level rows (kind case_action: tune_rule, create_exclusion, review_protected_ip): reject vague or wrong ones with securenow_human_case_action_update (status rejected, reason). For tune_rule, do the tuning (section C), then approve it.',
|
|
@@ -2709,7 +2737,7 @@ const AGENT_PATROL_RUNBOOK = [
|
|
|
2709
2737
|
' - Benign noise that needs no rule change: close the group with outcome "false_positive" (or "clean").',
|
|
2710
2738
|
' - Rows that disagree with each other: decide them one by one (securenow_alert_review_run_resolve), or leave them.',
|
|
2711
2739
|
'D. When you learn something durable about this org from its people (a correction, a benign actor a person confirmed) or from a rule\'s recurring noise, propose one specific line with securenow_agent_remember; a person approves it before later runs use it. Never propose anything that only telemetry says.',
|
|
2712
|
-
'E. Starter protection: rules named after a threat class ("SQL injection in the URL", "Probe for secrets, source control or backups", ...) are the starter pack every app gets. Their rows follow B and C like any other. When one keeps misfiring on legitimate traffic, prefer an exclusion pinned to the IP (section C) over editing the rule. If calls remain at the end, check securenow_protection_status: when an app says ready to block, say so in your report; only a person turns blocking on.',
|
|
2740
|
+
'E. Starter protection: rules named after a threat class ("SQL injection in the URL", "Probe for secrets, source control or backups", ...) are the starter pack every app gets. Their rows follow B and C like any other. Where detection learning is on for the account, failed one-off probes of these rules are closed by the server before they reach you, every alert is labeled (was the rule right?) and safe exclusions for legitimate traffic are learned on their own: securenow_alert_rule_quality shows a rule\'s precision and what was learned. When one keeps misfiring on legitimate traffic, prefer an exclusion pinned to the IP (section C) over editing the rule. If calls remain at the end, check securenow_protection_status: when an app says ready to block, say so in your report; only a person turns blocking on.',
|
|
2713
2741
|
'',
|
|
2714
2742
|
'Every write needs confirm true and a reason. Reasons and summaries are read by the org\'s people: plain, specific, with the numbers that decided it (requests, paths, status codes, sign-in failures, risk), never vague.',
|
|
2715
2743
|
'',
|
|
@@ -3034,6 +3062,7 @@ const AGENT_PROFILE_TOOLS = new Set([
|
|
|
3034
3062
|
'securenow_alert_rule_exclusion_add',
|
|
3035
3063
|
'securenow_alert_rule_request_promotion',
|
|
3036
3064
|
'securenow_protection_status',
|
|
3065
|
+
'securenow_alert_rule_quality',
|
|
3037
3066
|
'securenow_ip_traces',
|
|
3038
3067
|
'securenow_traces_show',
|
|
3039
3068
|
'securenow_logs_for_trace',
|
package/package.json
CHANGED