securenow 8.16.0 → 8.18.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/NPM_README.md +8 -1
- package/SKILL-CLI.md +33 -2
- package/cli/client.js +4 -1
- package/cli/human.js +63 -0
- package/cli/monitor.js +43 -0
- package/cli/security.js +357 -16
- package/cli.js +33 -11
- package/esm-detect.js +57 -0
- package/mcp/catalog.js +173 -15
- package/mcp/server.js +17 -2
- package/package.json +3 -2
- package/register.js +3 -7
- package/tracing.js +7 -13
package/NPM_README.md
CHANGED
|
@@ -352,6 +352,11 @@ npx securenow alerts rules create \
|
|
|
352
352
|
--severity high \
|
|
353
353
|
--nlp "single IP flooding /api/auth/signin or /api/auth/callback"
|
|
354
354
|
|
|
355
|
+
# What each run did, run one now, and why a rule didn't alert on an address (new in 8.17)
|
|
356
|
+
npx securenow alerts rules runs <rule-id>
|
|
357
|
+
npx securenow alerts rules run <rule-id>
|
|
358
|
+
npx securenow alerts rules why <rule-id> --ip 203.0.113.50 --since 2h
|
|
359
|
+
|
|
355
360
|
# View / manage alert rules, channels, and history
|
|
356
361
|
npx securenow alerts rules
|
|
357
362
|
npx securenow alerts rules show <rule-id>
|
|
@@ -363,7 +368,7 @@ npx securenow alerts channels
|
|
|
363
368
|
npx securenow alerts history --limit 20
|
|
364
369
|
```
|
|
365
370
|
|
|
366
|
-
Your detection SQL scopes app keys with the `__USER_APP_KEYS__` placeholder and selects an `ip` column for per-IP aggregation and remediation. SecureNow stores the query, runs it on the rule's schedule (default every 15 min), and routes matches to your in-app channel (override with `--channel <id>`). Requires an API key with `alerts:write`.
|
|
371
|
+
Your detection SQL scopes app keys with the `__USER_APP_KEYS__` placeholder and selects an `ip` column for per-IP aggregation and remediation (SQL without one is refused: it would never notify). SecureNow stores the query, runs it once right away and then on the rule's schedule (default every 15 min), and routes matches to your in-app channel (override with `--channel <id>`). The throttle sends one alert per address every 15 minutes by default; other addresses still alert. Requires an API key with `alerts:write`.
|
|
367
372
|
|
|
368
373
|
### IP Intelligence & Blocklist
|
|
369
374
|
|
|
@@ -617,6 +622,8 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
617
622
|
| | `alerts rules` | List rules (status, apps, schedule) |
|
|
618
623
|
| | `alerts rules show <id>` | Rule detail |
|
|
619
624
|
| | `alerts rules update <id> --applications-all` / `--apps k1,k2` | Application scope |
|
|
625
|
+
| | `alerts rules runs <id>` / `run <id>` | Run history (outcome of each run) / run a periodic rule now |
|
|
626
|
+
| | `alerts rules why <id> --ip <ip>` | Why the rule did or didn't alert on an address |
|
|
620
627
|
| | `alerts channels` | Alert channels |
|
|
621
628
|
| | `alerts history` | Alert history |
|
|
622
629
|
| **Investigate** | `ip <addr>` | IP intelligence |
|
package/SKILL-CLI.md
CHANGED
|
@@ -28,6 +28,22 @@ npx -p securenow securenow-mcp
|
|
|
28
28
|
|
|
29
29
|
The MCP server reads admin/control-plane auth from `.securenow/admin.json` and SDK runtime app credentials from `.securenow/runtime.json`, with legacy `.securenow/credentials.json` still supported. Admin/global tools use admin auth; runtime-scoped read tools can use the runtime API key where its scopes allow it. Write tools require `confirm:true` plus a reason.
|
|
30
30
|
|
|
31
|
+
**For agents, prefer the hosted MCP** (`https://api.securenow.ai/mcp`, OAuth sign-in, no install and always the current tools). With the local stdio server, warm the package once so the first connection doesn't time out on the download (`npx -y securenow@latest --version`), and give the client a longer connect timeout (Claude Code: `MCP_TIMEOUT=120000`).
|
|
32
|
+
|
|
33
|
+
**Tool profiles** (stdio server): `securenow mcp --profile <name>` (or `SECURENOW_MCP_PROFILE=<name>`) serves only the tools one job needs, so the agent carries less context. Every profile keeps `securenow_auth_status`, `securenow_apps_list` and `securenow_apps_info`; calling a tool outside the profile returns an error naming the profile.
|
|
34
|
+
|
|
35
|
+
| Profile | Tools | What's in it |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `all` (default) | 112 | everything |
|
|
38
|
+
| `investigator` | 58 | every read-only tool, plus forensics question/chat/confirm |
|
|
39
|
+
| `detection` | 47 | `securenow_alert_rule*` (draft, create, test, run, runs, explain, versions, exclusions, request promotion…), traces, logs, forensics, IP intelligence, notifications list/get |
|
|
40
|
+
| `responder` | 77 | notifications, human queue, alert reviews, blocklist, allowlist, trusted IPs, rate limits, challenges, automations, firewall, IP intelligence, plus rule get/runs/explain and traces/logs for context |
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
claude mcp add securenow-investigator -- npx -y securenow@latest mcp --profile investigator
|
|
44
|
+
codex mcp add securenow-detection -- npx securenow mcp --profile detection
|
|
45
|
+
```
|
|
46
|
+
|
|
31
47
|
### Authenticate
|
|
32
48
|
|
|
33
49
|
```bash
|
|
@@ -213,8 +229,11 @@ securenow traces analyze <traceId> --env production # AI-powered trace analysi
|
|
|
213
229
|
securenow logs [--app <key>] [--env production] [--limit N] [--minutes M] [--level error|warn|info]
|
|
214
230
|
securenow logs list --app my-app --env production --minutes 30 --level error
|
|
215
231
|
securenow logs trace <traceId> --env production # logs correlated to a specific trace
|
|
232
|
+
securenow logs errors --app my-app --since 6h [--warn] # what is failing: error lines grouped by message (ids/numbers normalized), counts, first/last seen, sample traces
|
|
216
233
|
```
|
|
217
234
|
|
|
235
|
+
For "what's broken right now?", start with `logs errors` (MCP `securenow_logs_errors`): one call returns the distinct failures, not hundreds of raw lines.
|
|
236
|
+
|
|
218
237
|
### Notifications
|
|
219
238
|
|
|
220
239
|
```bash
|
|
@@ -294,7 +313,11 @@ securenow alerts rules set-sql <id> --sql @new.sql # replace a CUSTOM rule's S
|
|
|
294
313
|
securenow alerts rules delete <id> --yes # delete a custom rule (system rules: disable instead)
|
|
295
314
|
securenow alerts rules update <id> --applications-all # all current & future apps
|
|
296
315
|
securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
|
|
297
|
-
securenow alerts rules test <id> --
|
|
316
|
+
securenow alerts rules test <id> --wait # dry-run an existing rule's query (default; --mode live inserts 6 synthetic attack requests)
|
|
317
|
+
securenow alerts rules change <id> "raise the threshold to 20" # rewrite a rule from English: checked, shown before/after, applied as a new version
|
|
318
|
+
securenow alerts rules runs <id> [--since 1h] # what each run saw and why it did or didn't notify (+ next scheduled run)
|
|
319
|
+
securenow alerts rules run <id> # run a periodic rule now and print the run (--no-wait to just queue it)
|
|
320
|
+
securenow alerts rules why <id> --ip <ip> [--since 1h] # why the rule did or didn't alert on one address: verdict + findings
|
|
298
321
|
securenow alerts rules dry-run-query <id> --sql @candidate.sql --app <key> --wait # candidate test against a specific host rule
|
|
299
322
|
securenow alerts rules tune-query <id> --sql @candidate.sql --reason "Preserve exploit detector, remove benign broad match" --apply-globally --yes
|
|
300
323
|
securenow alerts rules exclusions <id> list # embedded rule exclusions
|
|
@@ -311,7 +334,15 @@ right tenant-scope column per table: logs (`signoz_logs.distributed_logs_v2`) us
|
|
|
311
334
|
`resources_string['service.name'] IN (__USER_APP_KEYS__)`, traces
|
|
312
335
|
(`signoz_traces.distributed_signoz_index_v3`) use `` `resource_string_service$$name` IN (__USER_APP_KEYS__) ``.
|
|
313
336
|
|
|
314
|
-
`alerts rules create` flags: `--name` (required); one of `--sql <sql|@file|->` or `--query-mapping-id <id>`; one of `--apps k1,k2` or `--applications-all`; optional `--description`, `--nlp` (plain-English intent), `--category`, `--severity critical|high|medium|low`, `--schedule <cron>` (default `*/15 * * * *`), `--throttle-minutes N` / `--no-throttle`, `--execution-mode scheduled|instant|hybrid`, `--channel id1,id2` (defaults to your in-app SecureNow channel). Detection SQL must scope app keys via the `__USER_APP_KEYS__` placeholder and select an `ip` column for per-IP aggregation/remediation. Requires `alerts:write`.
|
|
337
|
+
`alerts rules create` flags: `--name` (required); one of `--sql <sql|@file|->` or `--query-mapping-id <id>`; one of `--apps k1,k2` or `--applications-all`; optional `--description`, `--nlp` (plain-English intent), `--category`, `--severity critical|high|medium|low`, `--schedule <cron>` (default `*/15 * * * *`), `--throttle-minutes N` / `--no-throttle`, `--execution-mode scheduled|instant|hybrid`, `--channel id1,id2` (defaults to your in-app SecureNow channel). Detection SQL must scope app keys via the `__USER_APP_KEYS__` placeholder and select an `ip` column for per-IP aggregation/remediation: create and set-sql refuse SQL with no ip column (it would never notify) unless `--allow-no-ip`. A new periodic rule runs once right away. Requires `alerts:write`.
|
|
338
|
+
|
|
339
|
+
**Throttle = one alert per rule AND address** (8.17): an address alerted on within the window (default 15 min) is left out of the next runs' notifications, and other addresses still alert. Alert storms keep one alert per rule per window.
|
|
340
|
+
|
|
341
|
+
**When a rule stays quiet,** don't guess: `alerts rules runs <id>` shows every run's outcome — `notified`, `no_rows`, `throttled`, `suppressed_blocked` (already blocked, often by another rule's instant block), `suppressed_trusted`, `no_ip` (rows without an ip column), `storm`, `auto_resolved` (dismissed by an exclusion, trusted IP or verified bot), `skipped`, `error` — and which addresses were left out and why. `alerts rules why <id> --ip <ip>` ends in a verdict such as "Detected at 15:06:31 but not notified: 203.0.113.50 was already blocked by instant rule X". `--yes` on `from-text` refuses to create a rule whose check matches your data right now (it would alert at once, usually a bad baseline); review the rows, then `--yes --force` if they are a real attack.
|
|
342
|
+
|
|
343
|
+
MCP parity for quiet rules: `securenow_alert_rule_runs` (run history), `securenow_alert_rule_run_now` (write, confirm) and `securenow_alert_rule_explain` (`ip`, `since`: verdict + findings).
|
|
344
|
+
|
|
345
|
+
**Agents don't promote rules; they ask.** When a test-mode rule has proved itself, an agent runs `securenow alerts rules request-promotion <id> --reason "..." --evidence "..."` (MCP `securenow_alert_rule_request_promotion`). A person sees it on the rules page or with `securenow human promotions` and runs `securenow human approve-promotion <request-id> --yes` (the rule goes to prod, recorded as a rule version naming the approver) or `deny-promotion <request-id> --reason "..."`. API keys and MCP tokens can ask but not decide.
|
|
315
346
|
|
|
316
347
|
MCP parity for noisy alert-rule reviews:
|
|
317
348
|
|
package/cli/client.js
CHANGED
|
@@ -56,13 +56,16 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
if (res.statusCode === 403) {
|
|
58
58
|
const msg = parsed?.error || parsed?.message || 'Access denied';
|
|
59
|
-
|
|
59
|
+
const err = new CLIError(`${msg}. Admin auth is connected, but this user/plan may lack permission for this control-plane operation. Runtime app connection is unrelated.`, 403);
|
|
60
|
+
if (parsed?.code) err.code = parsed.code;
|
|
61
|
+
reject(err);
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
if (res.statusCode >= 400) {
|
|
63
65
|
const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
|
|
64
66
|
const details = parsed?.details || parsed?.unauthorizedKeys;
|
|
65
67
|
const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
|
|
68
|
+
if (parsed?.code) err.code = parsed.code;
|
|
66
69
|
reject(err);
|
|
67
70
|
return;
|
|
68
71
|
}
|
package/cli/human.js
CHANGED
|
@@ -569,6 +569,66 @@ async function work(args, flags) {
|
|
|
569
569
|
console.log('');
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
// ── Rule promotion requests ──
|
|
573
|
+
// Agents create rules in test mode and ask a person to promote them. A person approves (the rule
|
|
574
|
+
// goes to prod, recorded as a rule version) or denies here or on the dashboard's rules page.
|
|
575
|
+
|
|
576
|
+
async function promotions(args, flags) {
|
|
577
|
+
requireAuth();
|
|
578
|
+
const status = typeof flags.status === 'string' ? flags.status : 'pending';
|
|
579
|
+
const s = ui.spinner('Fetching promotion requests');
|
|
580
|
+
let data;
|
|
581
|
+
try {
|
|
582
|
+
data = await api.get('/alert-rules/promotion-requests', { query: { status, limit: flags.limit || 50 } });
|
|
583
|
+
s.stop(`Found ${(data.requests || []).length} ${status === 'all' ? '' : `${status} `}request${(data.requests || []).length === 1 ? '' : 's'}`);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
s.fail('Failed to fetch promotion requests');
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
if (flags.json) { ui.json(data); return; }
|
|
589
|
+
const requests = data.requests || [];
|
|
590
|
+
if (requests.length === 0) { ui.info('No promotion requests waiting.'); return; }
|
|
591
|
+
console.log('');
|
|
592
|
+
for (const r of requests) {
|
|
593
|
+
console.log(` ${ui.c.bold(r.ruleName || r.ruleId)} ${ui.c.dim(r.id)} ${r.status}`);
|
|
594
|
+
console.log(` asked by ${r.requestedBy?.name || r.requestedBy?.kind || 'unknown'} ${ui.timeAgo(r.createdAt)}: ${r.reason}`);
|
|
595
|
+
if (r.evidence) console.log(ui.c.dim(` evidence: ${ui.truncate(String(r.evidence).replace(/\s+/g, ' '), 160)}`));
|
|
596
|
+
if (r.status !== 'pending') console.log(ui.c.dim(` ${r.status} by ${r.decidedByName || 'someone'}${r.decisionNote ? `: ${r.decisionNote}` : ''}`));
|
|
597
|
+
}
|
|
598
|
+
console.log('');
|
|
599
|
+
if (requests.some((r) => r.status === 'pending')) {
|
|
600
|
+
console.log(ui.c.dim(' Approve: securenow human approve-promotion <id> --yes [--note "..."] Deny: securenow human deny-promotion <id> --reason "..."'));
|
|
601
|
+
console.log(ui.c.dim(' Before approving, check what it would arm: securenow alerts rules show <ruleId> and securenow alerts rules runs <ruleId>'));
|
|
602
|
+
console.log('');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function decidePromotion(args, flags, decision) {
|
|
607
|
+
requireAuth();
|
|
608
|
+
const id = args[0];
|
|
609
|
+
const verb = decision === 'approve' ? 'approve-promotion' : 'deny-promotion';
|
|
610
|
+
if (!id) {
|
|
611
|
+
ui.error(`Usage: securenow human ${verb} <request-id> ${decision === 'approve' ? '--yes [--note "..."]' : '--reason "..."'}`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
if (decision === 'approve' && !flags.yes && !flags.force) {
|
|
615
|
+
ui.warn('Approving moves the rule to prod: its instant block, matching automations and the AI pipeline can act on its alerts.');
|
|
616
|
+
const ok = await ui.confirm('Approve this promotion?');
|
|
617
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
618
|
+
}
|
|
619
|
+
const note = flags.note || flags.reason || '';
|
|
620
|
+
const s = ui.spinner(decision === 'approve' ? 'Approving' : 'Denying');
|
|
621
|
+
try {
|
|
622
|
+
const data = await api.post(`/alert-rules/promotion-requests/${id}/${decision}`, { note });
|
|
623
|
+
s.stop(decision === 'approve' ? `Approved: "${data.alertRule?.name || data.request?.ruleName}" is in prod` : 'Denied: the rule stays in test mode');
|
|
624
|
+
for (const warning of data.warnings || []) ui.warn(warning);
|
|
625
|
+
if (flags.json) ui.json(data);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
s.fail(`Failed to ${decision} the promotion`);
|
|
628
|
+
throw err;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
572
632
|
module.exports = {
|
|
573
633
|
list,
|
|
574
634
|
show,
|
|
@@ -578,4 +638,7 @@ module.exports = {
|
|
|
578
638
|
report,
|
|
579
639
|
prompt,
|
|
580
640
|
work,
|
|
641
|
+
promotions,
|
|
642
|
+
approvePromotion: (args, flags) => decidePromotion(args, flags, 'approve'),
|
|
643
|
+
denyPromotion: (args, flags) => decidePromotion(args, flags, 'deny'),
|
|
581
644
|
};
|
package/cli/monitor.js
CHANGED
|
@@ -294,6 +294,48 @@ async function logsList(args, flags) {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// Error lines grouped by message (ids and numbers normalized): what is failing, how often, since when.
|
|
298
|
+
async function logsErrors(args, flags) {
|
|
299
|
+
requireAuth();
|
|
300
|
+
const appKey = resolveApp(flags);
|
|
301
|
+
if (!appKey) {
|
|
302
|
+
ui.error('No app specified. Use --app <key> or set a default.');
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
const query = { appKeys: appKey, since: flags.since || '1h', limit: flags.limit || 20 };
|
|
306
|
+
if (flags.warn) query.includeWarn = 'true';
|
|
307
|
+
addEnvironment(query, flags);
|
|
308
|
+
const s = ui.spinner('Grouping error logs');
|
|
309
|
+
let data;
|
|
310
|
+
try {
|
|
311
|
+
data = await api.get('/logs/errors', { query });
|
|
312
|
+
const kind = data.includeWarn ? 'error/warning' : 'error';
|
|
313
|
+
s.stop(`${data.total} ${kind} line${data.total === 1 ? '' : 's'} in ${data.distinct} group${data.distinct === 1 ? '' : 's'}`);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
s.fail('Failed to summarize error logs');
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
if (flags.json) { ui.json(data); return; }
|
|
319
|
+
const groups = data.groups || [];
|
|
320
|
+
console.log('');
|
|
321
|
+
if (groups.length === 0) { ui.info(`No ${data.includeWarn ? 'warnings or errors' : 'errors'} since ${data.window?.from}.`); return; }
|
|
322
|
+
for (const g of groups) {
|
|
323
|
+
const last = g.lastSeen ? ui.timeAgo(g.lastSeen) : '';
|
|
324
|
+
console.log(` ${ui.c.red(String(g.occurrences).padStart(5))} ${ui.truncate(String(g.sample || g.signature).replace(/\s+/g, ' '), 110)}`);
|
|
325
|
+
const meta = [
|
|
326
|
+
(g.severities || []).join('/'),
|
|
327
|
+
(g.environments || []).join(', '),
|
|
328
|
+
g.exceptionType,
|
|
329
|
+
last && `last ${last}`,
|
|
330
|
+
g.sampleTraceIds?.[0] && `trace ${g.sampleTraceIds[0]}`,
|
|
331
|
+
].filter(Boolean).join(' · ');
|
|
332
|
+
if (meta) console.log(ui.c.dim(` ${meta}`));
|
|
333
|
+
}
|
|
334
|
+
console.log('');
|
|
335
|
+
console.log(ui.c.dim(' A group\'s requests: securenow logs trace <traceId> Ask why: securenow forensics chat --app <key> "<question>"'));
|
|
336
|
+
console.log('');
|
|
337
|
+
}
|
|
338
|
+
|
|
297
339
|
async function logsTrace(args, flags) {
|
|
298
340
|
requireAuth();
|
|
299
341
|
const traceId = args[0];
|
|
@@ -499,6 +541,7 @@ module.exports = {
|
|
|
499
541
|
tracesAnalyze,
|
|
500
542
|
logsList,
|
|
501
543
|
logsTrace,
|
|
544
|
+
logsErrors,
|
|
502
545
|
notificationsList,
|
|
503
546
|
notificationsRead,
|
|
504
547
|
notificationsReadAll,
|