securenow 8.22.0 → 8.24.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/cli/init.js CHANGED
@@ -5,7 +5,26 @@ const path = require('path');
5
5
  const ui = require('./ui');
6
6
  const config = require('./config');
7
7
 
8
+ // The import sits inside the nodejs block, without webpackIgnore: Next.js drops the block from the
9
+ // Edge build and traces securenow and its dependencies into standalone output (webpackIgnore hid
10
+ // them from tracing, so a standalone image shipped without OpenTelemetry and every request failed).
11
+ // The try/catch keeps the app serving if SecureNow cannot start.
8
12
  const INSTRUMENTATION = `export async function register() {
13
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
14
+ try {
15
+ const securenowNext = await import('securenow/nextjs');
16
+ const registerSecureNow = securenowNext.registerSecureNow || securenowNext.default?.registerSecureNow;
17
+ registerSecureNow({ captureBody: true });
18
+ await import('securenow/nextjs-auto-capture');
19
+ } catch (error) {
20
+ console.error('[securenow] SecureNow did not start; the app is running without it:', error);
21
+ }
22
+ }
23
+ }
24
+ `;
25
+
26
+ // What init wrote before 8.24.0: replaced automatically when a file still holds exactly this.
27
+ const LEGACY_INSTRUMENTATION = `export async function register() {
9
28
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;
10
29
 
11
30
  const securenowNext = await import(/* webpackIgnore: true */ 'securenow/nextjs');
@@ -15,6 +34,10 @@ const INSTRUMENTATION = `export async function register() {
15
34
  }
16
35
  `;
17
36
 
37
+ function isLegacyInstrumentation(content) {
38
+ return String(content || '').replace(/\r\n/g, '\n').trim() === LEGACY_INSTRUMENTATION.trim();
39
+ }
40
+
18
41
  function detectProject(dir) {
19
42
  const pkgPath = path.join(dir, 'package.json');
20
43
  if (!fs.existsSync(pkgPath)) return { framework: 'unknown' };
@@ -78,6 +101,7 @@ async function init(_args, flags) {
78
101
  } else {
79
102
  initNode(project);
80
103
  }
104
+ checkPm2Ecosystem(dir);
81
105
 
82
106
  console.log('');
83
107
  ui.success('Setup complete.');
@@ -112,7 +136,10 @@ async function initNextJs(dir, project, flags) {
112
136
  const ext = useTs ? 'ts' : 'js';
113
137
 
114
138
  const existing = findInstrumentationFile(dir);
115
- if (existing) {
139
+ if (existing && isLegacyInstrumentation(fs.readFileSync(existing, 'utf8'))) {
140
+ fs.writeFileSync(existing, INSTRUMENTATION, 'utf8');
141
+ ui.success(`Updated ${path.relative(dir, existing)}: standalone builds now include SecureNow's dependencies`);
142
+ } else if (existing) {
116
143
  ui.info(`instrumentation file already exists: ${path.relative(dir, existing)}`);
117
144
  printAgentPrompt('instrumentation', path.basename(existing), nextMajor(project), project);
118
145
  } else {
@@ -239,7 +266,7 @@ function printAgentPrompt(kind, filename, major, project) {
239
266
  ? `Use the project scripts for verification when appropriate: ${commands}. Ask before starting long-running dev/start servers, and ask which command to use if these scripts are not the right customer workflow.`
240
267
  : 'Ask the customer which command starts, builds, and tests this app because package.json does not expose an obvious script.',
241
268
  kind === 'instrumentation'
242
- ? `Merge this into ${filename}: in register(), return unless process.env.NEXT_RUNTIME === "nodejs"; then dynamically import "securenow/nextjs" and "securenow/nextjs-auto-capture" with /* webpackIgnore: true */ so Next does not bundle OpenTelemetry internals. Preserve all existing instrumentation.`
269
+ ? `Merge this into ${filename}: inside register(), in an if (process.env.NEXT_RUNTIME === "nodejs") { ... } block, wrapped in try/catch so the app keeps running if SecureNow cannot start, dynamically import "securenow/nextjs" (call its registerSecureNow({ captureBody: true })) and then "securenow/nextjs-auto-capture". Do not add webpackIgnore comments and do not use an early return: keeping the imports inside the nodejs block lets Next.js drop them from the Edge build and trace securenow's dependencies into standalone output. Preserve all existing instrumentation.`
243
270
  : null,
244
271
  kind === 'next-config' && major >= 15
245
272
  ? `Update ${filename} while preserving existing config: add securenow to serverExternalPackages, e.g. serverExternalPackages: [...(existing || []), "securenow"], and include SecureNow runtime files for standalone output with outputFileTracingIncludes: { "/*": ["./node_modules/securenow/**/*"] }.`
@@ -267,6 +294,19 @@ function initNuxt(dir) {
267
294
  }
268
295
  }
269
296
 
297
+ // In PM2 cluster mode Node resolves a bare `-r securenow/register` from the PM2 daemon's folder,
298
+ // so workers started from elsewhere (pm2 resurrect at boot) crash with "Cannot find module".
299
+ function checkPm2Ecosystem(dir) {
300
+ for (const name of ['ecosystem.config.cjs', 'ecosystem.config.js', 'ecosystem.config.mjs']) {
301
+ const file = path.join(dir, name);
302
+ if (!fs.existsSync(file)) continue;
303
+ const content = fs.readFileSync(file, 'utf8');
304
+ if (content.includes('securenow/register') && !/require\.resolve\(\s*['"]securenow\/register['"]\s*\)/.test(content)) {
305
+ ui.warn(`${name}: pass the preload as an absolute path, node_args: ['-r', require.resolve('securenow/register')]. A bare '-r securenow/register' is resolved from the PM2 daemon's folder in cluster mode, and the workers crash when the daemon starts elsewhere (pm2 resurrect at boot).`);
306
+ }
307
+ }
308
+ }
309
+
270
310
  function initNode(project) {
271
311
  const scripts = project.pkg.scripts || {};
272
312
  const startScript = scripts.start || '';
@@ -293,4 +333,6 @@ function initNode(project) {
293
333
  }
294
334
  }
295
335
 
296
- module.exports = { init };
336
+ module.exports = {
337
+ INSTRUMENTATION,
338
+ isLegacyInstrumentation, init };
package/cli/today.js ADDED
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ // The dashboard's one view in the terminal (GET /overview): is every app covered, what happened, what needs you,
4
+ // the top attackers and what SecureNow did. `securenow attacker <ip>` explains one address
5
+ // (GET /overview/attackers/:ip). The same words as the dashboard.
6
+ const { api, requireAuth } = require('./client');
7
+ const ui = require('./ui');
8
+
9
+ const OUTCOMES = {
10
+ stopped: 'Stopped',
11
+ not_a_threat: 'Not a threat',
12
+ no_action: 'No action needed',
13
+ needs_you: 'Needs you',
14
+ being_checked: 'Being checked',
15
+ watching: 'Watching',
16
+ };
17
+ const LEVELS = { malicious: 'Malicious', blocked: 'Known attacker', suspicious: 'Suspicious', low: 'Low risk', unknown: 'Not scored', benign: 'Looks benign', insufficient_data: 'Not enough data', never_block: 'Never blocked' };
18
+ const MODES = { blocking: 'Blocking', monitoring: 'Monitoring', off: 'Off' };
19
+
20
+ function count(n) {
21
+ if (n === null || n === undefined) return '—';
22
+ return Number(n).toLocaleString('en-US');
23
+ }
24
+
25
+ function when(value) {
26
+ if (!value) return '';
27
+ const date = new Date(value);
28
+ const sameDay = date.toDateString() === new Date().toDateString();
29
+ return sameDay
30
+ ? date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })
31
+ : date.toLocaleString('en-GB', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
32
+ }
33
+
34
+ function level(item) {
35
+ const label = LEVELS[item.level] || item.level || '';
36
+ const text = item.score !== null && item.score !== undefined ? `${label} ${Math.round(item.score)}` : label;
37
+ return item.level === 'malicious' || item.level === 'blocked' ? ui.c.red(text) : item.level === 'suspicious' ? ui.c.yellow(text) : ui.c.dim(text);
38
+ }
39
+
40
+ function hoursFrom(flags) {
41
+ const since = String(flags.since || '24h');
42
+ const match = since.match(/^(\d+)\s*([hd])$/i);
43
+ if (!match) return 24;
44
+ const hours = Number(match[1]) * (match[2].toLowerCase() === 'd' ? 24 : 1);
45
+ return Math.min(168, Math.max(1, hours));
46
+ }
47
+
48
+ async function today(args, flags) {
49
+ requireAuth();
50
+ const hours = hoursFrom(flags);
51
+ const s = ui.spinner('Reading what happened');
52
+ let data;
53
+ try {
54
+ data = await api.get('/overview', { query: { hours, attackers: flags.attackers || 8 } });
55
+ s.stop('SecureNow');
56
+ } catch (err) {
57
+ s.fail('Could not read the overview');
58
+ throw err;
59
+ }
60
+ if (flags.json) { ui.json(data); return; }
61
+
62
+ const totals = data.totals || {};
63
+ console.log('');
64
+ console.log(` ${ui.c.bold(data.headline || '')} ${ui.c.dim(data.agent?.label || '')}`);
65
+ console.log(` ${ui.c.dim(`Last ${hours >= 48 ? `${Math.round(hours / 24)} days` : `${hours} hours`}:`)} ${count(totals.attackRequests)} attack requests · ${count(totals.attackers)} attackers · ${ui.c.green(`${count(totals.stopped)} stopped`)} · got through: ${totals.gotThrough === null || totals.gotThrough === undefined ? '—' : count(totals.gotThrough)}`);
66
+
67
+ console.log('');
68
+ const cards = data.needsYou || [];
69
+ console.log(` ${ui.c.bold(`Needs you (${data.needsYouTotal || 0})`)}`);
70
+ if (cards.length === 0) console.log(` ${ui.c.green('Nothing needs you.')} ${ui.c.dim(`SecureNow handled ${count(totals.handled)} things.`)}`);
71
+ for (const card of cards) {
72
+ console.log(` ${ui.c.yellow('!')} ${card.title}`);
73
+ if (card.detail) console.log(` ${ui.c.dim(card.detail)}`);
74
+ }
75
+
76
+ const attackers = data.attackers || [];
77
+ if (attackers.length) {
78
+ console.log('');
79
+ console.log(` ${ui.c.bold('Top attackers')}`);
80
+ for (const item of attackers) {
81
+ const status = item.blockedUntil ? (item.blockedUntil === 'permanent' ? 'Blocked' : `Blocked until ${when(item.blockedUntil)}`) : OUTCOMES[item.outcome] || '';
82
+ console.log(` ${item.ip.padEnd(16)} ${level(item)} ${ui.c.dim([item.what, (item.apps || []).map((app) => app.name).join(', '), `${count(item.requests)} req`].filter(Boolean).join(' · '))} ${status}`);
83
+ }
84
+ console.log(` ${ui.c.dim('Why an address was flagged: securenow attacker <ip>')}`);
85
+ }
86
+
87
+ const activity = (data.activity || []).slice(0, 8);
88
+ if (activity.length) {
89
+ console.log('');
90
+ console.log(` ${ui.c.bold('What SecureNow did')}`);
91
+ for (const item of activity) {
92
+ console.log(` ${ui.c.dim(when(item.at).padEnd(13))} ${item.text}${item.by === 'agent' ? ui.c.dim(' (agent)') : ''}`);
93
+ if (item.note) console.log(` ${' '.repeat(13)} ${ui.c.yellow(item.note)}`);
94
+ }
95
+ }
96
+
97
+ const apps = (data.apps || []).filter((app) => app.state === 'protected' || app.state === 'attention');
98
+ if (apps.length) {
99
+ console.log('');
100
+ console.log(` ${ui.c.bold('Apps')}`);
101
+ for (const app of apps) {
102
+ const dot = app.state === 'attention' ? ui.c.yellow('●') : ui.c.green('●');
103
+ const issues = (app.issues || []).map((issue) => issue.text).join('; ');
104
+ console.log(` ${dot} ${String(app.name).padEnd(30)} ${(MODES[app.mode] || app.mode).padEnd(11)} ${issues ? ui.c.yellow(issues) : ui.c.dim(`${count(app.stopped)} stopped`)}`);
105
+ }
106
+ }
107
+ console.log('');
108
+ }
109
+
110
+ async function attacker(args, flags) {
111
+ requireAuth();
112
+ const ip = String(args[0] || flags.ip || '').trim();
113
+ if (!ip) {
114
+ ui.error('Usage: securenow attacker <ip> [--json]');
115
+ process.exit(1);
116
+ }
117
+ const s = ui.spinner(`Reading ${ip}`);
118
+ let data;
119
+ try {
120
+ data = await api.get(`/overview/attackers/${encodeURIComponent(ip)}`);
121
+ s.stop(ip);
122
+ } catch (err) {
123
+ s.fail('Could not read this address');
124
+ throw err;
125
+ }
126
+ if (flags.json) { ui.json(data); return; }
127
+
128
+ const { verdict, reputation, state, summary } = data;
129
+ console.log('');
130
+ console.log(` ${ui.c.bold(ip)} ${level({ level: verdict.level, score: verdict.score })} ${ui.c.dim([reputation?.country, reputation?.isp, reputation?.usageType].filter(Boolean).join(' · '))}`);
131
+ const blocked = state.blocked
132
+ ? ui.c.green(`Blocked ${state.blocks.map((block) => `on ${block.app?.name || 'every app'}${block.until ? ` until ${when(block.until)}` : ''}`).join(', ')}`)
133
+ : state.neverBlock ? 'Never blocked (trusted)' : 'Not blocked';
134
+ console.log(` ${blocked}`);
135
+ console.log(` ${ui.c.dim(`First seen ${when(summary.firstSeen) || '—'} · last seen ${when(summary.lastSeen) || '—'} · ${count(summary.alerts)} detection(s) · ${count(summary.stopped)} stopped by the firewall in 7 days`)}`);
136
+ if (summary.categories?.length) console.log(` Tried: ${summary.categories.join(', ')}`);
137
+
138
+ console.log('');
139
+ console.log(` ${ui.c.bold('Why')}${verdict.analyzedAt ? ui.c.dim(` (analyzed ${when(verdict.analyzedAt)})`) : ''}`);
140
+ const signals = [...(verdict.signals?.malicious || []).map((line) => ui.c.red(`+ ${line}`)), ...(verdict.signals?.benign || []).map((line) => ui.c.green(`- ${line}`))];
141
+ if (signals.length) for (const line of signals) console.log(` ${line}`);
142
+ else console.log(` ${ui.c.dim(`No analysis yet: securenow agent verify ${ip}`)}`);
143
+
144
+ if (data.traffic) {
145
+ console.log('');
146
+ console.log(` ${ui.c.bold('Traffic, last 3 days')} ${count(data.traffic.requests)} requests on ${data.traffic.activeDays} day(s)`);
147
+ for (const signal of data.traffic.signals) {
148
+ const value = signal.format === 'percent' ? (signal.value === null ? '—' : `${Math.round(signal.value * 1000) / 10}%`) : count(signal.value);
149
+ const normal = signal.normal !== null && signal.normal !== undefined ? ui.c.dim(` (normal ${Math.round(signal.normal * 1000) / 10}%)`) : '';
150
+ console.log(` ${signal.label.padEnd(30)} ${signal.unusual ? ui.c.red(value) : value}${normal}${signal.unusual ? ui.c.red(' unusual') : ''}`);
151
+ }
152
+ }
153
+
154
+ if (data.alerts?.length) {
155
+ console.log('');
156
+ console.log(` ${ui.c.bold('Detections')}`);
157
+ for (const alert of data.alerts.slice(0, 10)) {
158
+ console.log(` ${ui.c.dim(when(alert.at).padEnd(13))} ${alert.title} ${ui.c.dim([alert.app?.name, `${count(alert.requests)} req`].filter(Boolean).join(' · '))} ${OUTCOMES[alert.outcome] || alert.status}`);
159
+ }
160
+ }
161
+
162
+ if (data.timeline?.length) {
163
+ console.log('');
164
+ console.log(` ${ui.c.bold('Timeline')}`);
165
+ for (const event of data.timeline.slice(0, 12)) console.log(` ${ui.c.dim(when(event.at).padEnd(13))} ${event.text}`);
166
+ }
167
+
168
+ if (data.questions?.length) {
169
+ console.log('');
170
+ console.log(` ${ui.c.bold('Ask the forensics chat')}`);
171
+ for (const question of data.questions) console.log(` ${ui.c.dim('securenow forensics chat')} "${question}"`);
172
+ }
173
+ console.log('');
174
+ }
175
+
176
+ module.exports = { today, attacker };
package/cli.js CHANGED
@@ -152,13 +152,14 @@ const COMMANDS = {
152
152
  sub: {
153
153
  runtime: {
154
154
  desc: 'Write tokenless runtime credentials for production file-based deploys',
155
- usage: 'securenow credentials runtime [--env production] [--out .securenow/credentials.production.json] [--stdout]',
155
+ usage: 'securenow credentials runtime [--env production] [--out .securenow/credentials.production.json] [--stdout] [--format env]',
156
156
  flags: {
157
157
  env: 'Deployment environment for this runtime file (default: production)',
158
158
  environment: 'Alias for --env',
159
159
  out: 'Output path (default: .securenow/credentials.<env>.json)',
160
160
  output: 'Alias for --out',
161
161
  stdout: 'Print JSON to stdout instead of writing a file',
162
+ format: 'env: print SECURENOW_CREDENTIALS=<base64> for hosts without file mounts (Vercel, Railway, Render, Heroku, Fly)',
162
163
  },
163
164
  run: (a, f) => require('./cli/credentials').runtime(a, f),
164
165
  },
@@ -457,6 +458,18 @@ const COMMANDS = {
457
458
  },
458
459
  defaultSub: 'status',
459
460
  },
461
+ today: {
462
+ desc: 'What happened, what SecureNow did and what needs you (the dashboard Overview)',
463
+ usage: 'securenow today [--since 24h|7d] [--attackers <n>] [--json]',
464
+ flags: { since: 'Look-back window, 1h to 7d (default 24h)', attackers: 'How many top attackers to list (default 8)', json: 'Output as JSON' },
465
+ run: (a, f) => require('./cli/today').today(a, f),
466
+ },
467
+ attacker: {
468
+ desc: 'Why SecureNow flagged an address: verdict, signals, traffic, detections, timeline, questions to ask',
469
+ usage: 'securenow attacker <ip> [--json]',
470
+ flags: { json: 'Output as JSON' },
471
+ run: (a, f) => require('./cli/today').attacker(a, f),
472
+ },
460
473
  protect: {
461
474
  desc: 'Starter protection per app: the rules every app gets, monitor or block mode',
462
475
  usage: 'securenow protect <status|enable|disable|block|monitor|learn> [--app <key>] [--json]',
@@ -911,7 +924,7 @@ function showHelp(commandName) {
911
924
  'Authentication': ['login', 'logout', 'whoami', 'credentials', 'admin', 'api-key'],
912
925
  'Applications': ['apps', 'app', 'init', 'status'],
913
926
  'Observe': ['traces', 'logs', 'analytics'],
914
- 'Detect & Respond': ['protect', 'agent', 'human', 'review', 'notifications', 'alerts', 'fp'],
927
+ 'Detect & Respond': ['today', 'attacker', 'protect', 'agent', 'human', 'review', 'notifications', 'alerts', 'fp'],
915
928
  'Investigate': ['ip', 'forensics'],
916
929
  'Firewall': ['firewall'],
917
930
  'Remediation': ['automation', 'ratelimit', 'challenge', 'blocklist', 'revoke', 'allowlist', 'trusted'],
package/crash-capture.js CHANGED
@@ -122,4 +122,16 @@ function installCrashCapture({ getLogger, flush, isSdkError, onSdkError, write }
122
122
  });
123
123
  }
124
124
 
125
- module.exports = { installCrashCapture, describeError, unhandledRejectionMode };
125
+ // Force-flush the global tracer provider. A crash used to ship the FATAL log but drop every
126
+ // span still waiting in the batch processor, which are usually the requests that led to it.
127
+ function flushTracerProvider() {
128
+ try {
129
+ const { trace } = require('@opentelemetry/api');
130
+ const provider = trace.getTracerProvider();
131
+ const target = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider;
132
+ if (target && typeof target.forceFlush === 'function') return Promise.resolve(target.forceFlush()).catch(() => {});
133
+ } catch (_) {}
134
+ return Promise.resolve();
135
+ }
136
+
137
+ module.exports = { installCrashCapture, describeError, unhandledRejectionMode, flushTracerProvider };
package/esm-detect.js CHANGED
@@ -38,7 +38,11 @@ function nearestPackageJson(startDir) {
38
38
 
39
39
  /** @returns {{ esm: boolean, reason: string|null }} */
40
40
  function detectEsmApp({ argv = process.argv, cwd = process.cwd(), env = process.env } = {}) {
41
- const entries = [argv && argv[1], env && env.pm_exec_path]
41
+ // A pm2 cluster worker only has pm2_env (JSON) at preload time, and its working directory is
42
+ // still the pm2 daemon's: take the app's folder and script from pm2 (see pm2-env.js).
43
+ const pm2 = require('./pm2-env').readPm2Env(env);
44
+ if (pm2 && pm2.cwd) cwd = pm2.cwd;
45
+ const entries = [argv && argv[1], pm2 && pm2.execPath]
42
46
  .filter((p) => typeof p === 'string' && p && !p.startsWith('-'))
43
47
  .map((p) => path.resolve(cwd, p));
44
48
 
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fastify route and error capture.
5
+ *
6
+ * The OpenTelemetry bundle dropped its Fastify instrumentation (moved to @fastify/otel), so a
7
+ * Fastify request span had no http.route and a failing handler left no exception event. Every
8
+ * Fastify instance (4.x and 5.x) is announced on the 'fastify.initialization' diagnostics
9
+ * channel; these hooks give it what the Express, Koa and Hapi instrumentations give theirs:
10
+ * - http.route on the request's server span, and the "GET /users/:id" span name, through the
11
+ * request's RPC metadata (the HTTP instrumentation reads it when the response finishes);
12
+ * - the handler's error recorded as an exception event on that span.
13
+ */
14
+
15
+ let installed = false;
16
+
17
+ function routeOf(request) {
18
+ try {
19
+ return (request.routeOptions && request.routeOptions.url) || request.routerPath || null;
20
+ } catch (_) {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function onInstance(message) {
26
+ const fastify = message && message.fastify;
27
+ if (!fastify || typeof fastify.addHook !== 'function') return;
28
+ let api;
29
+ let core;
30
+ try {
31
+ api = require('@opentelemetry/api');
32
+ core = require('@opentelemetry/core');
33
+ } catch (_) {
34
+ return;
35
+ }
36
+
37
+ fastify.addHook('onRequest', function securenowRoute(request, reply, done) {
38
+ try {
39
+ const route = routeOf(request);
40
+ if (route) {
41
+ const meta = core.getRPCMetadata(api.context.active());
42
+ if (meta && meta.type === core.RPCType.HTTP) meta.route = route;
43
+ const span = api.trace.getActiveSpan();
44
+ if (span) span.setAttribute('http.route', route);
45
+ }
46
+ } catch (_) {}
47
+ done();
48
+ });
49
+
50
+ fastify.addHook('onError', function securenowError(request, reply, error, done) {
51
+ try {
52
+ const span = api.trace.getActiveSpan();
53
+ if (span && error) span.recordException(error);
54
+ } catch (_) {}
55
+ done();
56
+ });
57
+ }
58
+
59
+ function installFastifyHooks() {
60
+ if (installed) return;
61
+ installed = true;
62
+ try {
63
+ require('node:diagnostics_channel').subscribe('fastify.initialization', onInstance);
64
+ } catch (_) {}
65
+ }
66
+
67
+ module.exports = { installFastifyHooks, routeOf };
package/mcp/catalog.js CHANGED
@@ -1315,6 +1315,34 @@ const TOOLS = [
1315
1315
  app: string('Optional application key; every app the caller can see when left out.'),
1316
1316
  }),
1317
1317
  },
1318
+ {
1319
+ name: 'securenow_overview',
1320
+ title: 'Overview: What Happened and What Needs You',
1321
+ description: 'The dashboard Overview for the last hours (default 24, at most 168, or since an ISO time). Whether each app is covered (data flowing, firewall on, detection monitoring or blocking, with what is missing); attack requests, attackers, requests the firewall stopped, and attacks that got through (whose impact check found possible or confirmed impact; null when none was checked), with attacks per hour; the decisions waiting for a person (block recommendations, coverage problems, apps ready to block, detection promotions); the top attackers (verdict, score, what they tried, apps, requests, whether blocked); and what SecureNow did (the agent\'s actions, automatic blocks, failed probes and rule-fix closes, learned exclusions).',
1322
+ scope: 'notifications:read',
1323
+ readOnly: true,
1324
+ method: 'GET',
1325
+ endpoint: '/overview',
1326
+ queryFields: ['hours', 'since', 'attackers'],
1327
+ inputSchema: objectSchema({
1328
+ hours: number('Look-back window in hours (default 24).', { minimum: 1, maximum: 168 }),
1329
+ since: string('Start of the window as an ISO time, at most 7 days ago; used instead of hours.'),
1330
+ attackers: number('How many top attackers to list (default 8).', { minimum: 1, maximum: 200 }),
1331
+ }),
1332
+ },
1333
+ {
1334
+ name: 'securenow_attacker',
1335
+ title: 'Attacker: Why an Address Was Flagged',
1336
+ description: 'One address across the caller\'s apps: the verdict and risk score with the signals behind them (from the latest verification), reputation, whether it is blocked (where and until when) or never blocked, first and last seen, what it tried, the detections it triggered in 30 days with their outcomes, its block history, a timeline of its events, its traffic over the last 3 days against the apps\' normal (error rate, share of traffic, distinct paths, scanner-path requests, attack payloads answered 2xx), and suggested questions for the forensics chat.',
1337
+ scope: 'notifications:read',
1338
+ readOnly: true,
1339
+ method: 'GET',
1340
+ endpoint: '/overview/attackers/:ip',
1341
+ pathParams: ['ip'],
1342
+ inputSchema: objectSchema({
1343
+ ip: string('IPv4 or IPv6 address.'),
1344
+ }, ['ip']),
1345
+ },
1318
1346
  {
1319
1347
  name: 'securenow_protection_enable',
1320
1348
  title: 'Turn Starter Protection On',
@@ -20,8 +20,11 @@
20
20
  * - config.capture.maxBodySize=10240 - Max body size in bytes (default: 10KB)
21
21
  * - config.capture.sensitiveFields=["field"] - Additional sensitive fields to redact
22
22
  *
23
- * The SDK reads these from .securenow/credentials.json in local development
24
- * and production. Legacy env vars are fallback-only for existing installs.
23
+ * The SDK reads these from .securenow/runtime.json (or SECURENOW_CREDENTIALS)
24
+ * in local development and production.
25
+ *
26
+ * Since 8.24.0 securenow/nextjs captures bodies at the HTTP layer; this module
27
+ * is the fallback for hosts where that layer does not see the request.
25
28
  *
26
29
  * Features:
27
30
  * - Zero code changes required in API routes