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 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> --mode dry_run --wait # validate an existing rule's query
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
- reject(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));
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,