securenow 8.22.0 → 8.23.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 CHANGED
@@ -208,10 +208,28 @@ Auto-detects framework (Next.js, Nuxt, Express, Fastify, Koa, Hapi, Node) from `
208
208
  ### Dashboard & Status
209
209
 
210
210
  ```bash
211
+ securenow today [--since 24h|7d] [--attackers 20] [--json] # the Overview: what happened, what SecureNow did, what needs you
212
+ securenow attacker <ip> [--json] # why an address was flagged: verdict, signals, traffic vs normal, detections, timeline
211
213
  securenow status [--app <key>] [--env local|production|all] # dashboard overview
212
214
  securenow analytics [--app <key>] # response analytics
213
215
  ```
214
216
 
217
+ `today` is the dashboard's home in the terminal:
218
+ - whether each app is covered (data flowing, firewall on, detection monitoring or blocking);
219
+ - attack requests, attackers, what the firewall stopped and what got through;
220
+ - the decisions waiting for you, the top attackers, and what SecureNow did (the agent's actions, automatic blocks,
221
+ closed probes).
222
+
223
+ `attacker <ip>` explains one address:
224
+ - the verdict and score with the signals behind them, and its reputation;
225
+ - where it is blocked;
226
+ - its last 3 days of traffic against your apps' normal (error rate, share of traffic, scanner probes, attack
227
+ payloads answered 2xx);
228
+ - the detections it triggered and what happened to each, a timeline, and questions to paste into
229
+ `securenow forensics chat`.
230
+
231
+ MCP: `securenow_overview` and `securenow_attacker` (read).
232
+
215
233
  ---
216
234
 
217
235
  ### Traces
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
@@ -457,6 +457,18 @@ const COMMANDS = {
457
457
  },
458
458
  defaultSub: 'status',
459
459
  },
460
+ today: {
461
+ desc: 'What happened, what SecureNow did and what needs you (the dashboard Overview)',
462
+ usage: 'securenow today [--since 24h|7d] [--attackers <n>] [--json]',
463
+ flags: { since: 'Look-back window, 1h to 7d (default 24h)', attackers: 'How many top attackers to list (default 8)', json: 'Output as JSON' },
464
+ run: (a, f) => require('./cli/today').today(a, f),
465
+ },
466
+ attacker: {
467
+ desc: 'Why SecureNow flagged an address: verdict, signals, traffic, detections, timeline, questions to ask',
468
+ usage: 'securenow attacker <ip> [--json]',
469
+ flags: { json: 'Output as JSON' },
470
+ run: (a, f) => require('./cli/today').attacker(a, f),
471
+ },
460
472
  protect: {
461
473
  desc: 'Starter protection per app: the rules every app gets, monitor or block mode',
462
474
  usage: 'securenow protect <status|enable|disable|block|monitor|learn> [--app <key>] [--json]',
@@ -911,7 +923,7 @@ function showHelp(commandName) {
911
923
  'Authentication': ['login', 'logout', 'whoami', 'credentials', 'admin', 'api-key'],
912
924
  'Applications': ['apps', 'app', 'init', 'status'],
913
925
  'Observe': ['traces', 'logs', 'analytics'],
914
- 'Detect & Respond': ['protect', 'agent', 'human', 'review', 'notifications', 'alerts', 'fp'],
926
+ 'Detect & Respond': ['today', 'attacker', 'protect', 'agent', 'human', 'review', 'notifications', 'alerts', 'fp'],
915
927
  'Investigate': ['ip', 'forensics'],
916
928
  'Firewall': ['firewall'],
917
929
  'Remediation': ['automation', 'ratelimit', 'challenge', 'blocklist', 'revoke', 'allowlist', 'trusted'],
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "8.22.0",
3
+ "version": "8.23.0",
4
4
  "mcpName": "ai.securenow/securenow",
5
5
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
6
6
  "type": "commonjs",