securenow 5.3.0 → 5.3.2
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 +281 -0
- package/README.md +139 -3
- package/cli/apps.js +4 -2
- package/cli/client.js +3 -1
- package/cli/monitor.js +79 -49
- package/cli/security.js +137 -86
- package/cli.js +4 -6
- package/package.json +1 -1
package/NPM_README.md
CHANGED
|
@@ -16,6 +16,7 @@ OpenTelemetry instrumentation library for Node.js applications. Send distributed
|
|
|
16
16
|
|
|
17
17
|
- [Installation](#installation)
|
|
18
18
|
- [Quick Start](#quick-start)
|
|
19
|
+
- [CLI — Command Line Interface](#cli--command-line-interface)
|
|
19
20
|
- [Framework-Specific Setup](#framework-specific-setup)
|
|
20
21
|
- [Express.js](#expressjs)
|
|
21
22
|
- [Next.js](#nextjs)
|
|
@@ -96,6 +97,286 @@ You'll see confirmation in the console:
|
|
|
96
97
|
|
|
97
98
|
---
|
|
98
99
|
|
|
100
|
+
## CLI — Command Line Interface
|
|
101
|
+
|
|
102
|
+
The `securenow` CLI gives you full access to the SecureNow platform from the terminal — no browser required for day-to-day workflows. Zero additional dependencies.
|
|
103
|
+
|
|
104
|
+
### Getting Started
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Log in (opens browser for OAuth)
|
|
108
|
+
npx securenow login
|
|
109
|
+
|
|
110
|
+
# Or use a token for CI/headless environments
|
|
111
|
+
npx securenow login --token <YOUR_JWT>
|
|
112
|
+
|
|
113
|
+
# Check who you're logged in as
|
|
114
|
+
npx securenow whoami
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Managing Applications
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# List all applications
|
|
121
|
+
npx securenow apps
|
|
122
|
+
|
|
123
|
+
# Create a new application
|
|
124
|
+
npx securenow apps create my-production-app --hosts api.example.com,app.example.com
|
|
125
|
+
|
|
126
|
+
# Get application details (including the app key)
|
|
127
|
+
npx securenow apps info <app-id>
|
|
128
|
+
|
|
129
|
+
# Set a default app so you don't need --app on every command
|
|
130
|
+
npx securenow apps default <app-key>
|
|
131
|
+
|
|
132
|
+
# Delete an application
|
|
133
|
+
npx securenow apps delete <app-id> --force
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Viewing Traces
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# List recent traces (uses default app, or specify --app)
|
|
140
|
+
npx securenow traces
|
|
141
|
+
npx securenow traces --app <key> --limit 50
|
|
142
|
+
|
|
143
|
+
# Show detailed spans for a trace
|
|
144
|
+
npx securenow traces show <traceId>
|
|
145
|
+
|
|
146
|
+
# AI-powered security analysis of a trace
|
|
147
|
+
npx securenow traces analyze <traceId>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Viewing Logs
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# List recent logs
|
|
154
|
+
npx securenow logs
|
|
155
|
+
npx securenow logs --app <key> --minutes 30 --level error
|
|
156
|
+
|
|
157
|
+
# Show logs for a specific trace
|
|
158
|
+
npx securenow logs trace <traceId>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Security Issues
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# List all issues
|
|
165
|
+
npx securenow issues
|
|
166
|
+
npx securenow issues --status open
|
|
167
|
+
|
|
168
|
+
# Show issue details with AI analysis
|
|
169
|
+
npx securenow issues show <issue-id>
|
|
170
|
+
|
|
171
|
+
# Resolve an issue
|
|
172
|
+
npx securenow issues resolve <issue-id>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Notifications
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# List notifications
|
|
179
|
+
npx securenow notifications
|
|
180
|
+
|
|
181
|
+
# Check unread count
|
|
182
|
+
npx securenow notifications unread
|
|
183
|
+
|
|
184
|
+
# Mark as read
|
|
185
|
+
npx securenow notifications read <id>
|
|
186
|
+
npx securenow notifications read-all
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Alerting
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
# View alert rules, channels, and history
|
|
193
|
+
npx securenow alerts rules
|
|
194
|
+
npx securenow alerts channels
|
|
195
|
+
npx securenow alerts history --limit 20
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### IP Intelligence & Blocklist
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Look up any IP — geo, abuse score, verdict, risk factors
|
|
202
|
+
npx securenow ip 203.0.113.42
|
|
203
|
+
|
|
204
|
+
# Show traces from a specific IP
|
|
205
|
+
npx securenow ip traces 203.0.113.42
|
|
206
|
+
|
|
207
|
+
# Manage the blocklist
|
|
208
|
+
npx securenow blocklist
|
|
209
|
+
npx securenow blocklist add 203.0.113.42 --reason "Brute force scanner"
|
|
210
|
+
npx securenow blocklist remove <id>
|
|
211
|
+
npx securenow blocklist stats
|
|
212
|
+
|
|
213
|
+
# Manage trusted IPs
|
|
214
|
+
npx securenow trusted
|
|
215
|
+
npx securenow trusted add 10.0.0.1 --label "Office VPN"
|
|
216
|
+
npx securenow trusted remove <id>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Forensic Queries
|
|
220
|
+
|
|
221
|
+
Ask questions in plain English — the AI translates them to SQL and runs them against your data.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Run a forensic query
|
|
225
|
+
npx securenow forensics "show top 10 attacking IPs in the last hour"
|
|
226
|
+
npx securenow forensics "which endpoints had 5xx errors today"
|
|
227
|
+
|
|
228
|
+
# Browse the saved query library
|
|
229
|
+
npx securenow forensics library
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### API Map
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# View all discovered API endpoints
|
|
236
|
+
npx securenow api-map
|
|
237
|
+
|
|
238
|
+
# API map statistics
|
|
239
|
+
npx securenow api-map stats
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Analytics & Dashboard
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# Response code analytics
|
|
246
|
+
npx securenow analytics --app <key>
|
|
247
|
+
|
|
248
|
+
# Full dashboard overview (apps, protection status, unread alerts)
|
|
249
|
+
npx securenow status
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Instances
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# List ClickHouse instances
|
|
256
|
+
npx securenow instances
|
|
257
|
+
|
|
258
|
+
# Test an instance connection
|
|
259
|
+
npx securenow instances test <id>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Configuration
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# View all config
|
|
266
|
+
npx securenow config get
|
|
267
|
+
|
|
268
|
+
# Set values
|
|
269
|
+
npx securenow config set apiUrl https://custom-api.example.com
|
|
270
|
+
npx securenow config set defaultApp <app-key>
|
|
271
|
+
npx securenow config set format json
|
|
272
|
+
|
|
273
|
+
# Show config file paths
|
|
274
|
+
npx securenow config path
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Config files are stored in `~/.securenow/`:
|
|
278
|
+
|
|
279
|
+
| File | Description |
|
|
280
|
+
|------|-------------|
|
|
281
|
+
| `config.json` | API URL, default app, output format |
|
|
282
|
+
| `credentials.json` | Auth token (file permissions: 0600) |
|
|
283
|
+
|
|
284
|
+
### Initialize Instrumentation
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# Interactive setup — creates instrumentation files for Next.js
|
|
288
|
+
npx securenow init
|
|
289
|
+
npx securenow init --ts --src --force
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Global Flags
|
|
293
|
+
|
|
294
|
+
Every command supports these flags:
|
|
295
|
+
|
|
296
|
+
| Flag | Short | Description |
|
|
297
|
+
|------|-------|-------------|
|
|
298
|
+
| `--json` | `-j` | Output as JSON for scripting and CI/CD |
|
|
299
|
+
| `--help` | | Show help for the command |
|
|
300
|
+
| `--app <key>` | | Override the default application key |
|
|
301
|
+
|
|
302
|
+
### Environment Variables
|
|
303
|
+
|
|
304
|
+
| Variable | Description |
|
|
305
|
+
|----------|-------------|
|
|
306
|
+
| `SECURENOW_API_URL` | Override the API base URL |
|
|
307
|
+
| `SECURENOW_DEBUG` | Show stack traces on errors |
|
|
308
|
+
| `NO_COLOR` | Disable colored output |
|
|
309
|
+
|
|
310
|
+
### CI/CD Integration
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# Authenticate with a token in CI
|
|
314
|
+
npx securenow login --token $SECURENOW_TOKEN
|
|
315
|
+
|
|
316
|
+
# Use --json for machine-readable output
|
|
317
|
+
npx securenow issues --json | jq '.[] | select(.severity == "critical")'
|
|
318
|
+
|
|
319
|
+
# Check for critical issues in a pipeline
|
|
320
|
+
ISSUES=$(npx securenow issues --json --status open)
|
|
321
|
+
CRITICAL=$(echo "$ISSUES" | jq '[.[] | select(.severity == "critical")] | length')
|
|
322
|
+
if [ "$CRITICAL" -gt "0" ]; then
|
|
323
|
+
echo "Found $CRITICAL critical issues!"
|
|
324
|
+
exit 1
|
|
325
|
+
fi
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Complete Command Reference
|
|
329
|
+
|
|
330
|
+
| Category | Command | Description |
|
|
331
|
+
|----------|---------|-------------|
|
|
332
|
+
| **Auth** | `login` | Authenticate via browser or `--token` |
|
|
333
|
+
| | `logout` | Clear credentials |
|
|
334
|
+
| | `whoami` | Show session info |
|
|
335
|
+
| **Apps** | `apps` | List applications |
|
|
336
|
+
| | `apps create <name>` | Create application |
|
|
337
|
+
| | `apps info <id>` | Application details |
|
|
338
|
+
| | `apps delete <id>` | Delete application |
|
|
339
|
+
| | `apps default <key>` | Set default app |
|
|
340
|
+
| **Observe** | `traces` | List traces |
|
|
341
|
+
| | `traces show <id>` | Trace details |
|
|
342
|
+
| | `traces analyze <id>` | AI trace analysis |
|
|
343
|
+
| | `logs` | List logs |
|
|
344
|
+
| | `logs trace <id>` | Logs for a trace |
|
|
345
|
+
| | `analytics` | Response analytics |
|
|
346
|
+
| | `status` | Dashboard overview |
|
|
347
|
+
| **Detect** | `issues` | List issues |
|
|
348
|
+
| | `issues show <id>` | Issue details |
|
|
349
|
+
| | `issues resolve <id>` | Resolve issue |
|
|
350
|
+
| | `notifications` | List notifications |
|
|
351
|
+
| | `notifications unread` | Unread count |
|
|
352
|
+
| | `notifications read <id>` | Mark read |
|
|
353
|
+
| | `notifications read-all` | Mark all read |
|
|
354
|
+
| | `alerts rules` | Alert rules |
|
|
355
|
+
| | `alerts channels` | Alert channels |
|
|
356
|
+
| | `alerts history` | Alert history |
|
|
357
|
+
| **Investigate** | `ip <addr>` | IP intelligence |
|
|
358
|
+
| | `ip traces <addr>` | Traces from IP |
|
|
359
|
+
| | `forensics "<query>"` | NL forensic query |
|
|
360
|
+
| | `forensics library` | Saved queries |
|
|
361
|
+
| | `api-map` | API endpoints |
|
|
362
|
+
| | `api-map stats` | API stats |
|
|
363
|
+
| **Remediate** | `blocklist` | Blocked IPs |
|
|
364
|
+
| | `blocklist add <ip>` | Block IP |
|
|
365
|
+
| | `blocklist remove <id>` | Unblock IP |
|
|
366
|
+
| | `blocklist stats` | Block stats |
|
|
367
|
+
| | `trusted` | Trusted IPs |
|
|
368
|
+
| | `trusted add <ip>` | Add trusted IP |
|
|
369
|
+
| | `trusted remove <id>` | Remove trusted |
|
|
370
|
+
| **Settings** | `instances` | List instances |
|
|
371
|
+
| | `instances test <id>` | Test connection |
|
|
372
|
+
| | `config get` | Show config |
|
|
373
|
+
| | `config set <k> <v>` | Set config value |
|
|
374
|
+
| | `config path` | Config file paths |
|
|
375
|
+
| | `init` | Setup instrumentation |
|
|
376
|
+
| | `version` | Show version |
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
99
380
|
## Framework-Specific Setup
|
|
100
381
|
|
|
101
382
|
### Express.js
|
package/README.md
CHANGED
|
@@ -36,7 +36,37 @@ SECURENOW_INSTANCE=http://your-otlp-collector:4318
|
|
|
36
36
|
npx securenow init
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
**Done!**
|
|
39
|
+
**Done!** See [Next.js Complete Guide](./docs/NEXTJS-GUIDE.md) for details.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### CLI — Manage Everything from the Terminal
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Authenticate
|
|
47
|
+
npx securenow login
|
|
48
|
+
|
|
49
|
+
# Create an app and get the key
|
|
50
|
+
npx securenow apps create my-app
|
|
51
|
+
|
|
52
|
+
# Set it as default so you don't need --app every time
|
|
53
|
+
npx securenow config set defaultApp <key>
|
|
54
|
+
|
|
55
|
+
# View traces, logs, issues
|
|
56
|
+
npx securenow traces
|
|
57
|
+
npx securenow logs
|
|
58
|
+
npx securenow issues
|
|
59
|
+
|
|
60
|
+
# IP intelligence, forensic queries, blocklist
|
|
61
|
+
npx securenow ip 1.2.3.4
|
|
62
|
+
npx securenow forensics "show top attacking IPs in the last hour"
|
|
63
|
+
npx securenow blocklist add 1.2.3.4 --reason "scanner"
|
|
64
|
+
|
|
65
|
+
# Full dashboard overview
|
|
66
|
+
npx securenow status
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Run `npx securenow help` for all commands. See the [CLI Reference](#cli-reference) below.
|
|
40
70
|
|
|
41
71
|
---
|
|
42
72
|
|
|
@@ -173,7 +203,113 @@ SecureNow automatically instruments:
|
|
|
173
203
|
|
|
174
204
|
---
|
|
175
205
|
|
|
176
|
-
##
|
|
206
|
+
## CLI Reference
|
|
207
|
+
|
|
208
|
+
After installing the package, the `securenow` CLI is available via `npx securenow` or globally after `npm install -g securenow`.
|
|
209
|
+
|
|
210
|
+
### Authentication
|
|
211
|
+
|
|
212
|
+
| Command | Description |
|
|
213
|
+
|---------|-------------|
|
|
214
|
+
| `securenow login` | Log in via browser (opens OAuth flow) |
|
|
215
|
+
| `securenow login --token <TOKEN>` | Log in with a token (for CI/headless) |
|
|
216
|
+
| `securenow logout` | Clear stored credentials |
|
|
217
|
+
| `securenow whoami` | Show current session info |
|
|
218
|
+
|
|
219
|
+
### Applications
|
|
220
|
+
|
|
221
|
+
| Command | Description |
|
|
222
|
+
|---------|-------------|
|
|
223
|
+
| `securenow apps` | List all applications |
|
|
224
|
+
| `securenow apps create <name>` | Create app and get the app key |
|
|
225
|
+
| `securenow apps info <id>` | Show application details |
|
|
226
|
+
| `securenow apps delete <id>` | Delete an application |
|
|
227
|
+
| `securenow apps default <key>` | Set default app for all commands |
|
|
228
|
+
|
|
229
|
+
### Observability
|
|
230
|
+
|
|
231
|
+
| Command | Description |
|
|
232
|
+
|---------|-------------|
|
|
233
|
+
| `securenow traces --app <key>` | List recent traces |
|
|
234
|
+
| `securenow traces show <traceId>` | Show trace spans |
|
|
235
|
+
| `securenow traces analyze <traceId>` | AI security analysis of a trace |
|
|
236
|
+
| `securenow logs --app <key>` | View logs (with `--minutes`, `--level`) |
|
|
237
|
+
| `securenow logs trace <traceId>` | View logs for a specific trace |
|
|
238
|
+
| `securenow analytics` | Response code analytics overview |
|
|
239
|
+
| `securenow status` | Full dashboard summary |
|
|
240
|
+
|
|
241
|
+
### Detect & Respond
|
|
242
|
+
|
|
243
|
+
| Command | Description |
|
|
244
|
+
|---------|-------------|
|
|
245
|
+
| `securenow issues` | List security issues |
|
|
246
|
+
| `securenow issues show <id>` | Show issue details and AI analysis |
|
|
247
|
+
| `securenow issues resolve <id>` | Mark an issue as resolved |
|
|
248
|
+
| `securenow notifications` | List notifications |
|
|
249
|
+
| `securenow notifications unread` | Show unread count |
|
|
250
|
+
| `securenow notifications read <id>` | Mark notification as read |
|
|
251
|
+
| `securenow notifications read-all` | Mark all as read |
|
|
252
|
+
| `securenow alerts rules` | List alert rules |
|
|
253
|
+
| `securenow alerts channels` | List alert channels |
|
|
254
|
+
| `securenow alerts history` | View alert history |
|
|
255
|
+
|
|
256
|
+
### Investigate
|
|
257
|
+
|
|
258
|
+
| Command | Description |
|
|
259
|
+
|---------|-------------|
|
|
260
|
+
| `securenow ip <address>` | IP intelligence lookup (geo, abuse score, verdict) |
|
|
261
|
+
| `securenow ip traces <address>` | Show traces originating from an IP |
|
|
262
|
+
| `securenow forensics "<query>"` | Natural language forensic query (NL to SQL) |
|
|
263
|
+
| `securenow forensics library` | View saved query library |
|
|
264
|
+
| `securenow api-map` | View discovered API endpoints |
|
|
265
|
+
| `securenow api-map stats` | API map statistics |
|
|
266
|
+
|
|
267
|
+
### Remediation
|
|
268
|
+
|
|
269
|
+
| Command | Description |
|
|
270
|
+
|---------|-------------|
|
|
271
|
+
| `securenow blocklist` | List blocked IPs |
|
|
272
|
+
| `securenow blocklist add <ip>` | Block an IP (`--reason <reason>`) |
|
|
273
|
+
| `securenow blocklist remove <id>` | Remove from blocklist |
|
|
274
|
+
| `securenow blocklist stats` | Blocklist statistics |
|
|
275
|
+
| `securenow trusted` | List trusted IPs |
|
|
276
|
+
| `securenow trusted add <ip>` | Add trusted IP (`--label <label>`) |
|
|
277
|
+
| `securenow trusted remove <id>` | Remove trusted IP |
|
|
278
|
+
|
|
279
|
+
### Settings
|
|
280
|
+
|
|
281
|
+
| Command | Description |
|
|
282
|
+
|---------|-------------|
|
|
283
|
+
| `securenow instances` | List ClickHouse instances |
|
|
284
|
+
| `securenow instances test <id>` | Test instance connection |
|
|
285
|
+
| `securenow config get` | Show all config values |
|
|
286
|
+
| `securenow config set <key> <value>` | Set a config value |
|
|
287
|
+
| `securenow config path` | Show config file locations |
|
|
288
|
+
| `securenow init` | Initialize instrumentation files |
|
|
289
|
+
| `securenow version` | Show CLI version |
|
|
290
|
+
|
|
291
|
+
### Global Flags
|
|
292
|
+
|
|
293
|
+
| Flag | Description |
|
|
294
|
+
|------|-------------|
|
|
295
|
+
| `--json` | Output as JSON (works on every command) |
|
|
296
|
+
| `--help` | Show help for any command |
|
|
297
|
+
| `--app <key>` | Specify app key (or set default with `config set defaultApp`) |
|
|
298
|
+
|
|
299
|
+
### Configuration
|
|
300
|
+
|
|
301
|
+
Credentials and settings are stored in `~/.securenow/`:
|
|
302
|
+
|
|
303
|
+
| File | Purpose |
|
|
304
|
+
|------|---------|
|
|
305
|
+
| `~/.securenow/config.json` | API URL, default app, preferences |
|
|
306
|
+
| `~/.securenow/credentials.json` | Auth token (restricted permissions) |
|
|
307
|
+
|
|
308
|
+
Override the API URL with `securenow config set apiUrl <url>` or the `SECURENOW_API_URL` environment variable.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Support
|
|
177
313
|
|
|
178
314
|
- **Website:** [securenow.ai](http://securenow.ai/)
|
|
179
315
|
- **Issues:** Report bugs and request features
|
|
@@ -181,6 +317,6 @@ SecureNow automatically instruments:
|
|
|
181
317
|
|
|
182
318
|
---
|
|
183
319
|
|
|
184
|
-
##
|
|
320
|
+
## License
|
|
185
321
|
|
|
186
322
|
ISC
|
package/cli/apps.js
CHANGED
|
@@ -9,7 +9,8 @@ async function list(args, flags) {
|
|
|
9
9
|
const s = ui.spinner('Fetching applications');
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
12
|
+
const data = await api.get('/applications');
|
|
13
|
+
const apps = data.applications || [];
|
|
13
14
|
s.stop(`Found ${apps.length} application${apps.length !== 1 ? 's' : ''}`);
|
|
14
15
|
console.log('');
|
|
15
16
|
|
|
@@ -101,7 +102,8 @@ async function info(args, flags) {
|
|
|
101
102
|
|
|
102
103
|
const s = ui.spinner('Fetching application details');
|
|
103
104
|
try {
|
|
104
|
-
const
|
|
105
|
+
const data = await api.get(`/applications/${id}`);
|
|
106
|
+
const app = data.application || data;
|
|
105
107
|
s.stop('Application details loaded');
|
|
106
108
|
|
|
107
109
|
if (flags.json) {
|
package/cli/client.js
CHANGED
|
@@ -60,7 +60,9 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
if (res.statusCode >= 400) {
|
|
62
62
|
const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
|
|
63
|
-
|
|
63
|
+
const details = parsed?.details || parsed?.unauthorizedKeys;
|
|
64
|
+
const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
|
|
65
|
+
reject(err);
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
66
68
|
|
package/cli/monitor.js
CHANGED
|
@@ -21,14 +21,14 @@ async function tracesList(args, flags) {
|
|
|
21
21
|
const s = ui.spinner('Fetching traces');
|
|
22
22
|
try {
|
|
23
23
|
const query = {
|
|
24
|
-
|
|
24
|
+
appKeys: appKey,
|
|
25
25
|
limit: flags.limit || 20,
|
|
26
26
|
};
|
|
27
|
-
if (flags.start) query.
|
|
28
|
-
if (flags.end) query.
|
|
27
|
+
if (flags.start) query.from = flags.start;
|
|
28
|
+
if (flags.end) query.to = flags.end;
|
|
29
29
|
|
|
30
|
-
const data = await api.get('/traces
|
|
31
|
-
const traces =
|
|
30
|
+
const data = await api.get('/traces', { query });
|
|
31
|
+
const traces = data.traces || [];
|
|
32
32
|
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
33
33
|
|
|
34
34
|
if (flags.json) { ui.json(traces); return; }
|
|
@@ -37,7 +37,7 @@ async function tracesList(args, flags) {
|
|
|
37
37
|
const rows = traces.map(t => [
|
|
38
38
|
ui.c.dim(ui.truncate(t.traceID || t.traceId || t._id, 16)),
|
|
39
39
|
t.operationName || t.name || t.serviceName || '—',
|
|
40
|
-
ui.httpStatusColor(t.statusCode || t.httpStatusCode || '—'),
|
|
40
|
+
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
41
41
|
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
42
42
|
t.httpMethod || t.method || '—',
|
|
43
43
|
ui.truncate(t.httpUrl || t.url || t.httpRoute || '', 40),
|
|
@@ -62,37 +62,31 @@ async function tracesShow(args, flags) {
|
|
|
62
62
|
|
|
63
63
|
const s = ui.spinner('Fetching trace details');
|
|
64
64
|
try {
|
|
65
|
-
const
|
|
65
|
+
const appKey = resolveApp(flags);
|
|
66
|
+
const traceQuery = appKey ? { appKeys: appKey } : {};
|
|
67
|
+
const data = await api.get(`/traces/${traceId}`, { query: traceQuery });
|
|
66
68
|
s.stop('Trace loaded');
|
|
67
69
|
|
|
68
70
|
if (flags.json) { ui.json(data); return; }
|
|
69
71
|
|
|
70
|
-
const
|
|
71
|
-
console.log('');
|
|
72
|
-
ui.heading(`Trace ${traceId}`);
|
|
72
|
+
const spans = data.spans || [];
|
|
73
73
|
console.log('');
|
|
74
|
+
ui.heading(`Trace ${data.traceId || traceId}`);
|
|
74
75
|
|
|
75
|
-
if (
|
|
76
|
-
ui.subheading(`Spans (${
|
|
76
|
+
if (spans.length) {
|
|
77
|
+
ui.subheading(`Spans (${spans.length})`);
|
|
77
78
|
console.log('');
|
|
78
|
-
const rows =
|
|
79
|
+
const rows = spans.map(span => [
|
|
79
80
|
ui.c.dim(ui.truncate(span.spanID || span.spanId, 16)),
|
|
80
81
|
span.operationName || span.name || '—',
|
|
81
|
-
ui.httpStatusColor(span.statusCode || '—'),
|
|
82
|
+
ui.httpStatusColor(span.statusCode || span.responseStatusCode || '—'),
|
|
82
83
|
ui.durationColor(span.durationNano ? span.durationNano / 1e6 : span.duration),
|
|
83
84
|
span.kind || '—',
|
|
84
85
|
]);
|
|
85
86
|
ui.table(['Span ID', 'Operation', 'Status', 'Duration', 'Kind'], rows);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (trace.rootSpan || trace.serviceName) {
|
|
87
|
+
} else {
|
|
89
88
|
console.log('');
|
|
90
|
-
ui.
|
|
91
|
-
['Service', trace.serviceName || '—'],
|
|
92
|
-
['Root Operation', trace.rootOperationName || trace.rootSpan?.operationName || '—'],
|
|
93
|
-
['Duration', trace.durationMs ? `${trace.durationMs}ms` : '—'],
|
|
94
|
-
['Timestamp', trace.startTime ? new Date(trace.startTime).toLocaleString() : '—'],
|
|
95
|
-
]);
|
|
89
|
+
ui.info('No spans found for this trace.');
|
|
96
90
|
}
|
|
97
91
|
console.log('');
|
|
98
92
|
} catch (err) {
|
|
@@ -116,11 +110,39 @@ async function tracesAnalyze(args, flags) {
|
|
|
116
110
|
|
|
117
111
|
if (flags.json) { ui.json(result); return; }
|
|
118
112
|
|
|
113
|
+
const analysis = result.analysis;
|
|
119
114
|
console.log('');
|
|
120
115
|
ui.heading('AI Trace Analysis');
|
|
121
116
|
console.log('');
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
|
|
118
|
+
if (typeof analysis === 'object' && analysis !== null) {
|
|
119
|
+
if (analysis.summary) {
|
|
120
|
+
ui.subheading('Summary');
|
|
121
|
+
console.log(`\n ${analysis.summary}\n`);
|
|
122
|
+
}
|
|
123
|
+
if (analysis.riskLevel) {
|
|
124
|
+
console.log(` ${ui.c.bold('Risk Level:')} ${ui.statusBadge(analysis.riskLevel)}\n`);
|
|
125
|
+
}
|
|
126
|
+
if (analysis.securityIssues?.length) {
|
|
127
|
+
ui.subheading('Security Issues');
|
|
128
|
+
console.log('');
|
|
129
|
+
analysis.securityIssues.forEach((issue, i) => {
|
|
130
|
+
console.log(` ${i + 1}. ${typeof issue === 'string' ? issue : issue.description || JSON.stringify(issue)}`);
|
|
131
|
+
});
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
|
134
|
+
if (analysis.recommendations?.length) {
|
|
135
|
+
ui.subheading('Recommendations');
|
|
136
|
+
console.log('');
|
|
137
|
+
analysis.recommendations.forEach((rec, i) => {
|
|
138
|
+
console.log(` ${i + 1}. ${typeof rec === 'string' ? rec : rec.description || JSON.stringify(rec)}`);
|
|
139
|
+
});
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
console.log(analysis || JSON.stringify(result, null, 2));
|
|
144
|
+
console.log('');
|
|
145
|
+
}
|
|
124
146
|
} catch (err) {
|
|
125
147
|
s.fail('Analysis failed');
|
|
126
148
|
throw err;
|
|
@@ -142,15 +164,15 @@ async function logsList(args, flags) {
|
|
|
142
164
|
const minutes = parseInt(flags.minutes || '60', 10);
|
|
143
165
|
const now = Date.now();
|
|
144
166
|
const query = {
|
|
145
|
-
|
|
167
|
+
appKeys: appKey,
|
|
146
168
|
limit: flags.limit || 50,
|
|
147
|
-
|
|
148
|
-
|
|
169
|
+
from: flags.start || new Date(now - minutes * 60 * 1000).toISOString(),
|
|
170
|
+
to: flags.end || new Date(now).toISOString(),
|
|
149
171
|
};
|
|
150
|
-
if (flags.level) query.
|
|
172
|
+
if (flags.level) query.severity = flags.level;
|
|
151
173
|
|
|
152
174
|
const data = await api.get('/logs', { query });
|
|
153
|
-
const logs =
|
|
175
|
+
const logs = data.logs || [];
|
|
154
176
|
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
155
177
|
|
|
156
178
|
if (flags.json) { ui.json(logs); return; }
|
|
@@ -190,7 +212,7 @@ async function logsTrace(args, flags) {
|
|
|
190
212
|
const s = ui.spinner('Fetching logs for trace');
|
|
191
213
|
try {
|
|
192
214
|
const data = await api.get(`/logs/trace/${traceId}`);
|
|
193
|
-
const logs =
|
|
215
|
+
const logs = data.logs || [];
|
|
194
216
|
s.stop(`Found ${logs.length} log${logs.length !== 1 ? 's' : ''}`);
|
|
195
217
|
|
|
196
218
|
if (flags.json) { ui.json(logs); return; }
|
|
@@ -225,10 +247,10 @@ async function issuesList(args, flags) {
|
|
|
225
247
|
if (flags.status) query.status = flags.status;
|
|
226
248
|
|
|
227
249
|
const data = await api.get('/issues', { query });
|
|
228
|
-
const issues =
|
|
250
|
+
const issues = data.issues || [];
|
|
229
251
|
s.stop(`Found ${issues.length} issue${issues.length !== 1 ? 's' : ''}`);
|
|
230
252
|
|
|
231
|
-
if (flags.json) { ui.json(
|
|
253
|
+
if (flags.json) { ui.json(data); return; }
|
|
232
254
|
|
|
233
255
|
console.log('');
|
|
234
256
|
const rows = issues.map(i => [
|
|
@@ -242,6 +264,9 @@ async function issuesList(args, flags) {
|
|
|
242
264
|
]);
|
|
243
265
|
|
|
244
266
|
ui.table(['ID', 'Severity', 'Status', 'Title', 'App', 'Count', 'Last Seen'], rows);
|
|
267
|
+
if (data.total != null) {
|
|
268
|
+
console.log(ui.c.dim(` Total: ${data.total}`));
|
|
269
|
+
}
|
|
245
270
|
console.log('');
|
|
246
271
|
} catch (err) {
|
|
247
272
|
s.fail('Failed to fetch issues');
|
|
@@ -259,7 +284,8 @@ async function issuesShow(args, flags) {
|
|
|
259
284
|
|
|
260
285
|
const s = ui.spinner('Fetching issue');
|
|
261
286
|
try {
|
|
262
|
-
const
|
|
287
|
+
const data = await api.get(`/issues/${id}`);
|
|
288
|
+
const issue = data.issue || data;
|
|
263
289
|
s.stop('Issue loaded');
|
|
264
290
|
|
|
265
291
|
if (flags.json) { ui.json(issue); return; }
|
|
@@ -320,8 +346,9 @@ async function notificationsList(args, flags) {
|
|
|
320
346
|
try {
|
|
321
347
|
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
322
348
|
const data = await api.get('/notifications', { query });
|
|
323
|
-
const notifications =
|
|
324
|
-
|
|
349
|
+
const notifications = data.notifications || [];
|
|
350
|
+
const pagination = data.pagination;
|
|
351
|
+
s.stop(`Found ${notifications.length} notification${notifications.length !== 1 ? 's' : ''}${pagination ? ` (page ${pagination.page}/${pagination.totalPages})` : ''}`);
|
|
325
352
|
|
|
326
353
|
if (flags.json) { ui.json(data); return; }
|
|
327
354
|
|
|
@@ -353,7 +380,7 @@ async function notificationsRead(args, flags) {
|
|
|
353
380
|
|
|
354
381
|
const s = ui.spinner('Marking as read');
|
|
355
382
|
try {
|
|
356
|
-
await api.put(`/notifications/${id}
|
|
383
|
+
await api.put(`/notifications/${id}/read`);
|
|
357
384
|
s.stop('Notification marked as read');
|
|
358
385
|
} catch (err) {
|
|
359
386
|
s.fail('Failed to mark notification');
|
|
@@ -377,7 +404,7 @@ async function notificationsUnread() {
|
|
|
377
404
|
requireAuth();
|
|
378
405
|
try {
|
|
379
406
|
const data = await api.get('/notifications/unread-count');
|
|
380
|
-
const count = data.count ??
|
|
407
|
+
const count = data.count ?? 0;
|
|
381
408
|
console.log(`\n ${ui.c.bold(String(count))} unread notification${count !== 1 ? 's' : ''}\n`);
|
|
382
409
|
} catch (err) {
|
|
383
410
|
throw err;
|
|
@@ -390,11 +417,12 @@ async function status(args, flags) {
|
|
|
390
417
|
requireAuth();
|
|
391
418
|
const s = ui.spinner('Fetching dashboard overview');
|
|
392
419
|
try {
|
|
393
|
-
const [
|
|
420
|
+
const [appsData, unreadData] = await Promise.all([
|
|
394
421
|
api.get('/applications'),
|
|
395
422
|
api.get('/notifications/unread-count').catch(() => ({ count: 0 })),
|
|
396
423
|
]);
|
|
397
424
|
|
|
425
|
+
const apps = appsData.applications || [];
|
|
398
426
|
s.stop('Dashboard loaded');
|
|
399
427
|
|
|
400
428
|
console.log('');
|
|
@@ -403,7 +431,7 @@ async function status(args, flags) {
|
|
|
403
431
|
|
|
404
432
|
ui.keyValue([
|
|
405
433
|
['Applications', String(apps.length)],
|
|
406
|
-
['Unread Alerts', String(
|
|
434
|
+
['Unread Alerts', String(unreadData.count ?? 0)],
|
|
407
435
|
]);
|
|
408
436
|
|
|
409
437
|
if (apps.length > 0) {
|
|
@@ -420,16 +448,18 @@ async function status(args, flags) {
|
|
|
420
448
|
const appKey = resolveApp(flags);
|
|
421
449
|
if (appKey) {
|
|
422
450
|
try {
|
|
423
|
-
const protectionData = await api.get('/applications/protection-status'
|
|
424
|
-
|
|
425
|
-
|
|
451
|
+
const protectionData = await api.get('/applications/protection-status');
|
|
452
|
+
const statuses = protectionData.statuses;
|
|
453
|
+
if (statuses && Object.keys(statuses).length > 0) {
|
|
454
|
+
ui.subheading('Protection Status');
|
|
426
455
|
console.log('');
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
ui.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
456
|
+
const rows = Object.entries(statuses).map(([id, s]) => [
|
|
457
|
+
ui.c.dim(ui.truncate(id, 12)),
|
|
458
|
+
s.protected ? ui.c.green('● protected') : ui.c.red('○ unprotected'),
|
|
459
|
+
String(s.traceCount || 0),
|
|
460
|
+
s.lastTrace ? ui.timeAgo(s.lastTrace) : ui.c.dim('—'),
|
|
461
|
+
]);
|
|
462
|
+
ui.table(['App ID', 'Status', 'Traces (15m)', 'Last Trace'], rows);
|
|
433
463
|
}
|
|
434
464
|
} catch {}
|
|
435
465
|
}
|
package/cli/security.js
CHANGED
|
@@ -15,7 +15,7 @@ async function alertRulesList(args, flags) {
|
|
|
15
15
|
const s = ui.spinner('Fetching alert rules');
|
|
16
16
|
try {
|
|
17
17
|
const data = await api.get('/alert-rules');
|
|
18
|
-
const rules =
|
|
18
|
+
const rules = data.alertRules || [];
|
|
19
19
|
s.stop(`Found ${rules.length} rule${rules.length !== 1 ? 's' : ''}`);
|
|
20
20
|
|
|
21
21
|
if (flags.json) { ui.json(rules); return; }
|
|
@@ -44,7 +44,7 @@ async function alertChannelsList(args, flags) {
|
|
|
44
44
|
const s = ui.spinner('Fetching alert channels');
|
|
45
45
|
try {
|
|
46
46
|
const data = await api.get('/alert-channels');
|
|
47
|
-
const channels =
|
|
47
|
+
const channels = data.alertChannels || [];
|
|
48
48
|
s.stop(`Found ${channels.length} channel${channels.length !== 1 ? 's' : ''}`);
|
|
49
49
|
|
|
50
50
|
if (flags.json) { ui.json(channels); return; }
|
|
@@ -73,10 +73,10 @@ async function alertHistoryList(args, flags) {
|
|
|
73
73
|
try {
|
|
74
74
|
const query = { limit: flags.limit || 20 };
|
|
75
75
|
const data = await api.get('/alert-history', { query });
|
|
76
|
-
const history =
|
|
77
|
-
s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}`);
|
|
76
|
+
const history = data.alerts || [];
|
|
77
|
+
s.stop(`Found ${history.length} alert${history.length !== 1 ? 's' : ''}${data.totalItems ? ` (${data.totalItems} total)` : ''}`);
|
|
78
78
|
|
|
79
|
-
if (flags.json) { ui.json(
|
|
79
|
+
if (flags.json) { ui.json(data); return; }
|
|
80
80
|
|
|
81
81
|
console.log('');
|
|
82
82
|
const rows = history.map(h => [
|
|
@@ -102,10 +102,10 @@ async function blocklistList(args, flags) {
|
|
|
102
102
|
const s = ui.spinner('Fetching blocklist');
|
|
103
103
|
try {
|
|
104
104
|
const data = await api.get('/blocklist');
|
|
105
|
-
const items =
|
|
106
|
-
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}`);
|
|
105
|
+
const items = data.blockedIps || [];
|
|
106
|
+
s.stop(`Found ${items.length} blocked IP${items.length !== 1 ? 's' : ''}${data.total ? ` (${data.total} total)` : ''}`);
|
|
107
107
|
|
|
108
|
-
if (flags.json) { ui.json(
|
|
108
|
+
if (flags.json) { ui.json(data); return; }
|
|
109
109
|
|
|
110
110
|
console.log('');
|
|
111
111
|
const rows = items.map(b => [
|
|
@@ -174,7 +174,8 @@ async function blocklistStats(args, flags) {
|
|
|
174
174
|
requireAuth();
|
|
175
175
|
const s = ui.spinner('Fetching blocklist stats');
|
|
176
176
|
try {
|
|
177
|
-
const
|
|
177
|
+
const data = await api.get('/blocklist/stats');
|
|
178
|
+
const stats = data.stats || data;
|
|
178
179
|
s.stop('Stats loaded');
|
|
179
180
|
|
|
180
181
|
if (flags.json) { ui.json(stats); return; }
|
|
@@ -182,8 +183,13 @@ async function blocklistStats(args, flags) {
|
|
|
182
183
|
console.log('');
|
|
183
184
|
ui.heading('Blocklist Statistics');
|
|
184
185
|
console.log('');
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
ui.keyValue([
|
|
187
|
+
['Total Active', String(stats.totalActive ?? '—')],
|
|
188
|
+
['Total Removed', String(stats.totalRemoved ?? '—')],
|
|
189
|
+
['Manual Blocks', String(stats.manualCount ?? '—')],
|
|
190
|
+
['Automation Blocks', String(stats.automationCount ?? '—')],
|
|
191
|
+
['Active Rules', String(stats.activeAutomationRules ?? '—')],
|
|
192
|
+
]);
|
|
187
193
|
console.log('');
|
|
188
194
|
} catch (err) {
|
|
189
195
|
s.fail('Failed to fetch stats');
|
|
@@ -198,7 +204,7 @@ async function trustedList(args, flags) {
|
|
|
198
204
|
const s = ui.spinner('Fetching trusted IPs');
|
|
199
205
|
try {
|
|
200
206
|
const data = await api.get('/trusted-ips');
|
|
201
|
-
const items =
|
|
207
|
+
const items = data.trustedIps || [];
|
|
202
208
|
s.stop(`Found ${items.length} trusted IP${items.length !== 1 ? 's' : ''}`);
|
|
203
209
|
|
|
204
210
|
if (flags.json) { ui.json(items); return; }
|
|
@@ -278,17 +284,40 @@ async function forensicsQuery(args, flags) {
|
|
|
278
284
|
const body = { query };
|
|
279
285
|
if (flags.instance) body.instanceId = flags.instance;
|
|
280
286
|
|
|
281
|
-
const job = await api.post('/forensics/
|
|
282
|
-
const jobId = job.jobId
|
|
287
|
+
const job = await api.post('/forensics/query', body);
|
|
288
|
+
const jobId = job.jobId;
|
|
289
|
+
|
|
290
|
+
if (!jobId) {
|
|
291
|
+
s.stop('Query complete');
|
|
292
|
+
if (flags.json) { ui.json(job); return; }
|
|
293
|
+
if (job.result) {
|
|
294
|
+
console.log('');
|
|
295
|
+
if (job.sqlquery) {
|
|
296
|
+
ui.subheading('Generated SQL');
|
|
297
|
+
console.log(`\n ${ui.c.dim(job.sqlquery)}\n`);
|
|
298
|
+
}
|
|
299
|
+
const data = job.result;
|
|
300
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
301
|
+
const headers = Object.keys(data[0]);
|
|
302
|
+
const rows = data.map(row => headers.map(h => String(row[h] ?? '')));
|
|
303
|
+
ui.table(headers, rows);
|
|
304
|
+
} else {
|
|
305
|
+
ui.json(data);
|
|
306
|
+
}
|
|
307
|
+
console.log('');
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
283
312
|
s.update('Processing query...');
|
|
284
313
|
|
|
285
314
|
let result;
|
|
286
315
|
const maxAttempts = 60;
|
|
287
316
|
for (let i = 0; i < maxAttempts; i++) {
|
|
288
317
|
await new Promise(r => setTimeout(r, 2000));
|
|
289
|
-
result = await api.get(`/forensics/
|
|
318
|
+
result = await api.get(`/forensics/query/status/${jobId}`);
|
|
290
319
|
if (result.status === 'completed' || result.status === 'failed') break;
|
|
291
|
-
s.update(`Processing query... (${i * 2}s)`);
|
|
320
|
+
s.update(`Processing query... (${(i + 1) * 2}s)`);
|
|
292
321
|
}
|
|
293
322
|
|
|
294
323
|
if (result.status === 'failed') {
|
|
@@ -308,14 +337,14 @@ async function forensicsQuery(args, flags) {
|
|
|
308
337
|
if (flags.json) { ui.json(result); return; }
|
|
309
338
|
|
|
310
339
|
console.log('');
|
|
311
|
-
if (result.
|
|
340
|
+
if (result.sqlquery) {
|
|
312
341
|
ui.subheading('Generated SQL');
|
|
313
|
-
console.log(`\n ${ui.c.dim(result.
|
|
342
|
+
console.log(`\n ${ui.c.dim(result.sqlquery)}\n`);
|
|
314
343
|
}
|
|
315
344
|
|
|
316
|
-
if (result.
|
|
317
|
-
const data = result.
|
|
318
|
-
ui.subheading(`Results (${Array.isArray(data) ? data.length : '?'} rows)`);
|
|
345
|
+
if (result.result) {
|
|
346
|
+
const data = result.result;
|
|
347
|
+
ui.subheading(`Results (${result.rowCount ?? (Array.isArray(data) ? data.length : '?')} rows)`);
|
|
319
348
|
console.log('');
|
|
320
349
|
|
|
321
350
|
if (Array.isArray(data) && data.length > 0) {
|
|
@@ -326,11 +355,6 @@ async function forensicsQuery(args, flags) {
|
|
|
326
355
|
ui.json(data);
|
|
327
356
|
}
|
|
328
357
|
}
|
|
329
|
-
|
|
330
|
-
if (result.explanation) {
|
|
331
|
-
ui.subheading('Explanation');
|
|
332
|
-
console.log(`\n ${result.explanation}\n`);
|
|
333
|
-
}
|
|
334
358
|
console.log('');
|
|
335
359
|
} catch (err) {
|
|
336
360
|
s.fail('Forensic query failed');
|
|
@@ -343,7 +367,7 @@ async function forensicsLibrary(args, flags) {
|
|
|
343
367
|
const s = ui.spinner('Fetching query library');
|
|
344
368
|
try {
|
|
345
369
|
const data = await api.get('/forensics/query-library');
|
|
346
|
-
const queries =
|
|
370
|
+
const queries = data.data || [];
|
|
347
371
|
s.stop(`Found ${queries.length} saved quer${queries.length !== 1 ? 'ies' : 'y'}`);
|
|
348
372
|
|
|
349
373
|
if (flags.json) { ui.json(queries); return; }
|
|
@@ -381,46 +405,42 @@ async function ipLookup(args, flags) {
|
|
|
381
405
|
if (flags.json) { ui.json(data); return; }
|
|
382
406
|
|
|
383
407
|
console.log('');
|
|
384
|
-
ui.heading(`IP Intelligence: ${ip}`);
|
|
408
|
+
ui.heading(`IP Intelligence: ${data.ip || ip}`);
|
|
385
409
|
console.log('');
|
|
386
410
|
|
|
387
|
-
const info = data.ipData || data.intel || data;
|
|
388
411
|
const pairs = [];
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
if (
|
|
392
|
-
if (
|
|
393
|
-
if (
|
|
394
|
-
if (
|
|
395
|
-
if (
|
|
396
|
-
if (
|
|
397
|
-
if (
|
|
398
|
-
if (
|
|
399
|
-
if (
|
|
400
|
-
if (
|
|
401
|
-
if (info.isCrawler != null) pairs.push(['Crawler', info.isCrawler ? ui.c.yellow('Yes') : 'No']);
|
|
402
|
-
if (info.threatLevel) pairs.push(['Threat Level', ui.statusBadge(info.threatLevel)]);
|
|
403
|
-
if (info.riskLevel) pairs.push(['Risk Level', ui.statusBadge(info.riskLevel)]);
|
|
412
|
+
if (data.countryName || data.countryCode) pairs.push(['Country', `${data.countryName || ''} ${data.countryCode ? `(${data.countryCode})` : ''}`.trim()]);
|
|
413
|
+
if (data.domain) pairs.push(['Domain', data.domain]);
|
|
414
|
+
if (data.isp) pairs.push(['ISP', data.isp]);
|
|
415
|
+
if (data.usageType) pairs.push(['Usage Type', data.usageType]);
|
|
416
|
+
if (data.abuseConfidenceScore != null) pairs.push(['Abuse Score', `${data.abuseConfidenceScore}/100`]);
|
|
417
|
+
if (data.securenowScore != null) pairs.push(['SecureNow Score', String(data.securenowScore)]);
|
|
418
|
+
if (data.verdict) pairs.push(['Verdict', data.verdict]);
|
|
419
|
+
if (data.isMalicious != null) pairs.push(['Malicious', data.isMalicious ? ui.c.red('Yes') : ui.c.green('No')]);
|
|
420
|
+
if (data.isBot != null) pairs.push(['Bot', data.isBot ? ui.c.yellow('Yes') : 'No']);
|
|
421
|
+
if (data.activityType) pairs.push(['Activity', data.activityType]);
|
|
422
|
+
if (data.totalReports != null) pairs.push(['Total Reports', String(data.totalReports)]);
|
|
423
|
+
if (data.lastReportedAt) pairs.push(['Last Reported', new Date(data.lastReportedAt).toLocaleString()]);
|
|
404
424
|
|
|
405
425
|
if (pairs.length) {
|
|
406
426
|
ui.keyValue(pairs);
|
|
407
|
-
} else {
|
|
408
|
-
ui.keyValue(Object.entries(info).slice(0, 20).map(([k, v]) => [k, String(v)]));
|
|
409
427
|
}
|
|
410
428
|
|
|
411
|
-
if (data.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
429
|
+
if (data.riskFactors?.length) {
|
|
430
|
+
ui.subheading('Risk Factors');
|
|
431
|
+
console.log('');
|
|
432
|
+
data.riskFactors.forEach(f => console.log(` • ${f}`));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (data.attackTypes?.length) {
|
|
436
|
+
ui.subheading('Attack Types');
|
|
437
|
+
console.log('');
|
|
438
|
+
data.attackTypes.forEach(a => console.log(` • ${a}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (data.summary) {
|
|
442
|
+
ui.subheading('Summary');
|
|
443
|
+
console.log(`\n ${data.summary}`);
|
|
424
444
|
}
|
|
425
445
|
console.log('');
|
|
426
446
|
} catch (err) {
|
|
@@ -440,16 +460,16 @@ async function ipTraces(args, flags) {
|
|
|
440
460
|
const s = ui.spinner(`Fetching traces for ${ip}`);
|
|
441
461
|
try {
|
|
442
462
|
const data = await api.get(`/ip/${ip}/traces`);
|
|
443
|
-
const traces =
|
|
463
|
+
const traces = data.traces || [];
|
|
444
464
|
s.stop(`Found ${traces.length} trace${traces.length !== 1 ? 's' : ''}`);
|
|
445
465
|
|
|
446
|
-
if (flags.json) { ui.json(
|
|
466
|
+
if (flags.json) { ui.json(data); return; }
|
|
447
467
|
|
|
448
468
|
console.log('');
|
|
449
469
|
const rows = traces.map(t => [
|
|
450
470
|
ui.c.dim(ui.truncate(t.traceID || t.traceId, 16)),
|
|
451
471
|
t.httpMethod || t.method || '—',
|
|
452
|
-
ui.httpStatusColor(t.statusCode || t.httpStatusCode || '—'),
|
|
472
|
+
ui.httpStatusColor(t.statusCode || t.httpStatusCode || t.responseStatusCode || '—'),
|
|
453
473
|
ui.truncate(t.httpUrl || t.url || '', 40),
|
|
454
474
|
ui.durationColor(t.durationNano ? t.durationNano / 1e6 : t.duration),
|
|
455
475
|
ui.timeAgo(t.timestamp),
|
|
@@ -469,22 +489,40 @@ async function apiMapList(args, flags) {
|
|
|
469
489
|
const s = ui.spinner('Fetching API map');
|
|
470
490
|
try {
|
|
471
491
|
const data = await api.get('/api-map');
|
|
472
|
-
const
|
|
473
|
-
s.stop(
|
|
492
|
+
const apiMap = data.apiMap;
|
|
493
|
+
s.stop('API map loaded');
|
|
474
494
|
|
|
475
495
|
if (flags.json) { ui.json(data); return; }
|
|
476
496
|
|
|
497
|
+
if (!apiMap) {
|
|
498
|
+
console.log('');
|
|
499
|
+
ui.info(data.message || 'No API map discovered yet. Run discovery from the dashboard.');
|
|
500
|
+
console.log('');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
477
504
|
console.log('');
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
505
|
+
if (apiMap.apps && typeof apiMap.apps === 'object') {
|
|
506
|
+
for (const [appName, appData] of Object.entries(apiMap.apps)) {
|
|
507
|
+
ui.subheading(appName);
|
|
508
|
+
console.log('');
|
|
509
|
+
const endpoints = appData.endpoints || [];
|
|
510
|
+
if (endpoints.length) {
|
|
511
|
+
const rows = endpoints.map(e => [
|
|
512
|
+
e.method || '—',
|
|
513
|
+
e.path || e.route || '—',
|
|
514
|
+
e.requestCount != null ? String(e.requestCount) : '—',
|
|
515
|
+
e.description || ui.c.dim('—'),
|
|
516
|
+
]);
|
|
517
|
+
ui.table(['Method', 'Path', 'Requests', 'Description'], rows);
|
|
518
|
+
} else {
|
|
519
|
+
ui.info('No endpoints discovered for this app.');
|
|
520
|
+
}
|
|
521
|
+
console.log('');
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
ui.json(apiMap);
|
|
525
|
+
}
|
|
488
526
|
} catch (err) {
|
|
489
527
|
s.fail('Failed to fetch API map');
|
|
490
528
|
throw err;
|
|
@@ -495,15 +533,30 @@ async function apiMapStats(args, flags) {
|
|
|
495
533
|
requireAuth();
|
|
496
534
|
const s = ui.spinner('Fetching API map stats');
|
|
497
535
|
try {
|
|
498
|
-
const
|
|
536
|
+
const data = await api.get('/api-map/stats');
|
|
537
|
+
const stats = data.stats;
|
|
499
538
|
s.stop('Stats loaded');
|
|
500
539
|
|
|
501
540
|
if (flags.json) { ui.json(stats); return; }
|
|
502
541
|
|
|
542
|
+
if (!stats) {
|
|
543
|
+
console.log('');
|
|
544
|
+
ui.info('No API map stats available.');
|
|
545
|
+
console.log('');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
503
549
|
console.log('');
|
|
504
550
|
ui.heading('API Map Statistics');
|
|
505
551
|
console.log('');
|
|
506
|
-
ui.keyValue(
|
|
552
|
+
ui.keyValue([
|
|
553
|
+
['Total Apps', String(stats.totalApps ?? '—')],
|
|
554
|
+
['Total Endpoints', String(stats.totalEndpoints ?? '—')],
|
|
555
|
+
['Total Requests', String(stats.totalRequests ?? '—')],
|
|
556
|
+
['Discovery Status', stats.discoveryStatus || '—'],
|
|
557
|
+
['Last Discovered', stats.lastDiscoveredAt ? new Date(stats.lastDiscoveredAt).toLocaleString() : '—'],
|
|
558
|
+
['Version', String(stats.version ?? '—')],
|
|
559
|
+
]);
|
|
507
560
|
console.log('');
|
|
508
561
|
} catch (err) {
|
|
509
562
|
s.fail('Failed to fetch stats');
|
|
@@ -518,7 +571,7 @@ async function instancesList(args, flags) {
|
|
|
518
571
|
const s = ui.spinner('Fetching instances');
|
|
519
572
|
try {
|
|
520
573
|
const data = await api.get('/instances');
|
|
521
|
-
const instances =
|
|
574
|
+
const instances = data.instances || [];
|
|
522
575
|
s.stop(`Found ${instances.length} instance${instances.length !== 1 ? 's' : ''}`);
|
|
523
576
|
|
|
524
577
|
if (flags.json) { ui.json(instances); return; }
|
|
@@ -529,10 +582,10 @@ async function instancesList(args, flags) {
|
|
|
529
582
|
inst.name || inst.host || '—',
|
|
530
583
|
inst.host || '—',
|
|
531
584
|
inst.port != null ? String(inst.port) : '—',
|
|
532
|
-
|
|
585
|
+
inst.linkedApps != null ? String(inst.linkedApps) : '—',
|
|
533
586
|
ui.timeAgo(inst.createdAt),
|
|
534
587
|
]);
|
|
535
|
-
ui.table(['ID', 'Name', 'Host', 'Port', '
|
|
588
|
+
ui.table(['ID', 'Name', 'Host', 'Port', 'Linked Apps', 'Added'], rows);
|
|
536
589
|
console.log('');
|
|
537
590
|
} catch (err) {
|
|
538
591
|
s.fail('Failed to fetch instances');
|
|
@@ -551,11 +604,10 @@ async function instancesTest(args, flags) {
|
|
|
551
604
|
const s = ui.spinner('Testing instance connection');
|
|
552
605
|
try {
|
|
553
606
|
const result = await api.post(`/instances/${id}/test`);
|
|
554
|
-
if (result.success
|
|
555
|
-
s.stop(
|
|
607
|
+
if (result.success) {
|
|
608
|
+
s.stop(`Connection successful${result.storageGb ? ` (${result.storageGb} GB storage)` : ''}`);
|
|
556
609
|
} else {
|
|
557
|
-
s.fail('
|
|
558
|
-
if (result.error) ui.error(result.error);
|
|
610
|
+
s.fail(result.message || 'Connection failed');
|
|
559
611
|
}
|
|
560
612
|
|
|
561
613
|
if (flags.json) ui.json(result);
|
|
@@ -569,11 +621,10 @@ async function instancesTest(args, flags) {
|
|
|
569
621
|
|
|
570
622
|
async function analytics(args, flags) {
|
|
571
623
|
requireAuth();
|
|
572
|
-
const appKey = resolveApp(flags);
|
|
573
624
|
const s = ui.spinner('Fetching analytics');
|
|
574
625
|
try {
|
|
575
626
|
const query = {};
|
|
576
|
-
|
|
627
|
+
const appKey = resolveApp(flags);
|
|
577
628
|
if (flags.instance) query.instanceId = flags.instance;
|
|
578
629
|
|
|
579
630
|
const endpoints = ['2xx-responses', '3xx-responses', '4xx-responses', '5xx-responses', '500-errors'];
|
|
@@ -596,7 +647,7 @@ async function analytics(args, flags) {
|
|
|
596
647
|
|
|
597
648
|
const pairs = endpoints.map((ep, i) => {
|
|
598
649
|
const val = results[i];
|
|
599
|
-
const count = val?.count ??
|
|
650
|
+
const count = val?.meta?.count ?? '—';
|
|
600
651
|
return [ep, String(count)];
|
|
601
652
|
});
|
|
602
653
|
ui.keyValue(pairs);
|
package/cli.js
CHANGED
|
@@ -377,13 +377,11 @@ async function main() {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
main().catch((err) => {
|
|
380
|
-
if (err.name
|
|
381
|
-
ui.error(err.message);
|
|
382
|
-
} else {
|
|
380
|
+
if (err.name !== 'CLIError') {
|
|
383
381
|
ui.error(err.message || 'An unexpected error occurred');
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
}
|
|
383
|
+
if (process.env.SECURENOW_DEBUG) {
|
|
384
|
+
console.error(err.stack || err);
|
|
387
385
|
}
|
|
388
386
|
process.exit(1);
|
|
389
387
|
});
|
package/package.json
CHANGED