securenow 8.8.0 → 8.10.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 +30 -0
- package/README.md +14 -0
- package/SKILL-API.md +23 -0
- package/SKILL-CLI.md +21 -3
- package/TELEMETRY-SQL.md +98 -0
- package/cli/security.js +170 -0
- package/cli/telemetry.js +199 -0
- package/cli.js +45 -5
- package/mcp/catalog.js +2233 -2187
- package/package.json +36 -28
- package/telemetry-schema.d.ts +16 -0
- package/telemetry-schema.js +3 -0
- package/telemetry-schema.json +188 -0
package/NPM_README.md
CHANGED
|
@@ -306,6 +306,36 @@ X-SecureNow-App-Key: <app-uuid>
|
|
|
306
306
|
|
|
307
307
|
CLI: `npx securenow event send auth.login.failure --user u_1 --ip 1.2.3.4 --attrs reason=bad_token`
|
|
308
308
|
|
|
309
|
+
### Build alert SQL from the installed telemetry contract
|
|
310
|
+
|
|
311
|
+
The NPM package ships the exact ClickHouse table/field contract used for alert-rule authoring.
|
|
312
|
+
Agents should read it instead of guessing SQL columns:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Offline: installed SDK schema, tenant scope, timestamp types, event mappings, and templates
|
|
316
|
+
npx securenow telemetry schema --json
|
|
317
|
+
|
|
318
|
+
# Connected app: names/types/counts observed in real telemetry (never attribute values)
|
|
319
|
+
npx securenow telemetry fields --app <app-key> --env production --since 24h --json
|
|
320
|
+
|
|
321
|
+
# Prove candidate SQL against the backend before creating anything
|
|
322
|
+
npx securenow alerts rules validate --sql @rule.sql --app <app-key>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The same local contract is available programmatically:
|
|
326
|
+
|
|
327
|
+
```js
|
|
328
|
+
const telemetrySchema = require('securenow/telemetry-schema');
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Use the installed schema to draft SQL, live field discovery to verify that optional/custom
|
|
332
|
+
fields are actually emitted, and `alerts rules validate` to prove the query compiles and runs.
|
|
333
|
+
A field absent from the selected live window is **unverified**, not necessarily unsupported.
|
|
334
|
+
Mark detections `NEEDS_INSTRUMENTATION` when the repository must add a `track()` event,
|
|
335
|
+
`NEEDS_BASELINE` when only the threshold remains unknown, and `READY` only after field
|
|
336
|
+
verification plus SQL validation. See `node_modules/securenow/TELEMETRY-SQL.md` for tables,
|
|
337
|
+
timestamp handling, privacy rules, templates, and the full test-to-production lifecycle.
|
|
338
|
+
|
|
309
339
|
```bash
|
|
310
340
|
# Create a custom detection rule from your own SQL (new in 8.1)
|
|
311
341
|
npx securenow alerts rules create \
|
package/README.md
CHANGED
|
@@ -223,6 +223,10 @@ npx securenow logs # tail logs
|
|
|
223
223
|
npx securenow status # dashboard summary
|
|
224
224
|
npx securenow doctor # diagnose config + connectivity
|
|
225
225
|
|
|
226
|
+
# Detection authoring contract
|
|
227
|
+
npx securenow telemetry schema --json
|
|
228
|
+
npx securenow telemetry fields --app <key> --env production --since 24h --json
|
|
229
|
+
|
|
226
230
|
# Security
|
|
227
231
|
npx securenow firewall status --env production
|
|
228
232
|
npx securenow blocklist add 1.2.3.4 --reason "scanner"
|
|
@@ -363,6 +367,16 @@ After install, the `securenow` CLI is available via `npx securenow` or globally
|
|
|
363
367
|
| `securenow analytics` | Response code analytics |
|
|
364
368
|
| `securenow status` | Dashboard summary |
|
|
365
369
|
|
|
370
|
+
### Detection authoring
|
|
371
|
+
|
|
372
|
+
| Command | Description |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `securenow telemetry schema --json` | Offline, versioned tables, fields, event mappings, tenant scope, and SQL requirements |
|
|
375
|
+
| `securenow telemetry fields --app <key> --env production --since 24h --json` | Fields, types, and custom event names actually observed for an authorized app; values are never returned |
|
|
376
|
+
| `securenow alerts rules validate --sql @rule.sql --app <key>` | Compile and dry-run candidate SQL before creating a rule |
|
|
377
|
+
|
|
378
|
+
The machine-readable contract is also exported as `require('securenow/telemetry-schema')`; the full workflow and SQL examples are in [`TELEMETRY-SQL.md`](./TELEMETRY-SQL.md).
|
|
379
|
+
|
|
366
380
|
### Detect & Respond
|
|
367
381
|
|
|
368
382
|
| Command | Description |
|
package/SKILL-API.md
CHANGED
|
@@ -453,6 +453,29 @@ Requires peer dep: `npm install @google-cloud/compute`
|
|
|
453
453
|
|
|
454
454
|
Logging is enabled by default (`config.logging.enabled: true`). Logs are exported to your OTLP collector alongside traces.
|
|
455
455
|
|
|
456
|
+
### Telemetry contract for alert-rule agents
|
|
457
|
+
|
|
458
|
+
The package exports a versioned, machine-readable description of the ClickHouse tables,
|
|
459
|
+
timestamp types, map access, tenant scope, automatic fields, custom-event mapping, client-IP
|
|
460
|
+
expression, and safe SQL templates:
|
|
461
|
+
|
|
462
|
+
```js
|
|
463
|
+
const telemetrySchema = require('securenow/telemetry-schema');
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The CLI exposes the same contract and can compare it with fields observed in the connected app:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
securenow telemetry schema --json
|
|
470
|
+
securenow telemetry fields --app <key> --env production --since 24h --json
|
|
471
|
+
securenow alerts rules validate --sql @rule.sql --app <key>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
The first command works offline. Live discovery returns field names/types/counts and event names,
|
|
475
|
+
never values. A coding agent should inspect the customer repository for routes and `track()` calls,
|
|
476
|
+
use the local contract to construct SQL, use live discovery to verify emitted fields, then validate
|
|
477
|
+
the candidate query. Full guidance is shipped in `TELEMETRY-SQL.md`.
|
|
478
|
+
|
|
456
479
|
### Get a Logger
|
|
457
480
|
|
|
458
481
|
```javascript
|
package/SKILL-CLI.md
CHANGED
|
@@ -250,9 +250,27 @@ MCP parity:
|
|
|
250
250
|
|
|
251
251
|
Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports. Notification IP investigations can include `metadata.matchedSubdetectors`; prefer that exact field over inferring which subdetector fired from the title or sample body.
|
|
252
252
|
|
|
253
|
-
### Alerts
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
### Alerts
|
|
254
|
+
|
|
255
|
+
Before authoring any alert SQL, load the contract shipped by the installed package and compare
|
|
256
|
+
it with fields actually observed for the selected application:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
securenow telemetry schema --json
|
|
260
|
+
securenow telemetry schema logs --json
|
|
261
|
+
securenow telemetry schema traces --json
|
|
262
|
+
securenow telemetry schema events --json
|
|
263
|
+
securenow telemetry fields --app <key> --env production --since 24h --source all --json
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`telemetry schema` is offline and versioned with the SDK. `telemetry fields` requires admin
|
|
267
|
+
auth and telemetry access; it returns names, map scopes, data types, occurrence counts, and
|
|
268
|
+
custom event types, but never customer attribute values. Absence means "not observed in this
|
|
269
|
+
window," not "unsupported." Use only documented fields, and label a rule `READY` only when
|
|
270
|
+
its fields were observed and `alerts rules validate` succeeds. Otherwise label it
|
|
271
|
+
`NEEDS_INSTRUMENTATION`, `NEEDS_BASELINE`, or `NEEDS_CUSTOMER_DECISION` as appropriate.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
256
274
|
securenow alerts # list alert rules (default)
|
|
257
275
|
securenow alerts rules # list alert rules (columns: Status, Applications, Schedule)
|
|
258
276
|
securenow alerts rules validate \ # dry-run a candidate query BEFORE creating any rule
|
package/TELEMETRY-SQL.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SecureNow telemetry and alert SQL contract
|
|
2
|
+
|
|
3
|
+
This document is the canonical starting point for humans and coding agents that construct SecureNow alert rules from a customer codebase. The same contract is available as machine-readable JSON:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx securenow telemetry schema --json
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The SDK contract tells you which tables and fields are supported. Live field discovery tells you which fields the connected application has actually emitted:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx securenow telemetry fields --app <app-key> --env production --since 24h --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Do not treat a documented optional or custom field as present until it appears in live discovery or the codebase contains the corresponding instrumentation.
|
|
16
|
+
|
|
17
|
+
## Alert-rule SQL requirements
|
|
18
|
+
|
|
19
|
+
- Use ClickHouse SQL.
|
|
20
|
+
- Query `signoz_logs.distributed_logs_v2` for custom events and logs.
|
|
21
|
+
- Query `signoz_traces.distributed_signoz_index_v3` for HTTP spans and traces.
|
|
22
|
+
- Keep `__USER_APP_KEYS__` in every customer-scoped query.
|
|
23
|
+
- For logs, scope with `resources_string['service.name'] IN (__USER_APP_KEYS__)`.
|
|
24
|
+
- For traces, scope with `` `resource_string_service$$name` IN (__USER_APP_KEYS__) ``.
|
|
25
|
+
- Bound the query with a time window.
|
|
26
|
+
- Return an `ip` column and exclude empty IPs when the rule can trigger IP remediation.
|
|
27
|
+
- Use the correct timestamp representation: log timestamps are UInt64 nanoseconds; trace timestamps are `DateTime64(9)`.
|
|
28
|
+
- Validate SQL against the connected SecureNow backend before creating the rule.
|
|
29
|
+
|
|
30
|
+
## Authoring workflow
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Read the exact contract shipped with the installed SDK.
|
|
34
|
+
npx securenow telemetry schema --json
|
|
35
|
+
|
|
36
|
+
# 2. Discover actual fields and types emitted by this app.
|
|
37
|
+
npx securenow telemetry fields --app <app-key> --env production --since 24h --json
|
|
38
|
+
|
|
39
|
+
# 3. Avoid duplicating existing coverage.
|
|
40
|
+
npx securenow alerts rules --json
|
|
41
|
+
|
|
42
|
+
# 4. Validate the candidate before any write.
|
|
43
|
+
npx securenow alerts rules validate --sql @rule.sql --app <app-key>
|
|
44
|
+
|
|
45
|
+
# 5. Create in detect-only mode and dry-run it.
|
|
46
|
+
npx securenow alerts rules create --name "Auth: OTP send abuse" --sql @rule.sql --apps <app-key>
|
|
47
|
+
npx securenow alerts rules test <rule-id> --mode dry_run --wait
|
|
48
|
+
|
|
49
|
+
# New custom rules start in detect-only test mode. Promote only after reviewing real matches and false positives.
|
|
50
|
+
npx securenow alerts rules promote <rule-id>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`telemetry schema` works offline. `telemetry fields`, alert-rule validation, and rule management require admin authentication and access to the selected app.
|
|
54
|
+
|
|
55
|
+
## Custom security events
|
|
56
|
+
|
|
57
|
+
The SDK accepts arbitrary event names and scalar custom attributes:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const { track } = require('securenow/events');
|
|
61
|
+
|
|
62
|
+
track('auth.otp.send.result', {
|
|
63
|
+
userId,
|
|
64
|
+
sessionId,
|
|
65
|
+
ip,
|
|
66
|
+
attributes: {
|
|
67
|
+
channel: 'sms',
|
|
68
|
+
provider: 'twilio',
|
|
69
|
+
provider_result: 'accepted',
|
|
70
|
+
destination_hash: sha256(normalizedDestination),
|
|
71
|
+
cost_units: 1,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Event properties are stored in the logs table:
|
|
77
|
+
|
|
78
|
+
| SDK property | Alert SQL field |
|
|
79
|
+
|---|---|
|
|
80
|
+
| Event type | `attributes_string['event.type']` |
|
|
81
|
+
| `userId` | `attributes_string['enduser.id']` |
|
|
82
|
+
| `sessionId` | `attributes_string['session.id']` |
|
|
83
|
+
| `ip` | `attributes_string['http.client_ip']` |
|
|
84
|
+
| `userAgent` | `attributes_string['http.user_agent']` |
|
|
85
|
+
| String custom attribute | `attributes_string['<key>']` |
|
|
86
|
+
| Numeric custom attribute | `attributes_number['<key>']` |
|
|
87
|
+
| Boolean custom attribute | `attributes_bool['<key>']` |
|
|
88
|
+
|
|
89
|
+
Never send raw passwords, tokens, OTPs, session secrets, email addresses, phone numbers, or other unnecessary personal data. Hash destinations when correlation is required.
|
|
90
|
+
|
|
91
|
+
## Readiness labels for generated detections
|
|
92
|
+
|
|
93
|
+
- `READY`: every queried field was observed and the SQL validation passed.
|
|
94
|
+
- `NEEDS_INSTRUMENTATION`: the codebase must add one or more `track()` events or attributes.
|
|
95
|
+
- `NEEDS_BASELINE`: fields and SQL are valid, but the threshold needs production traffic history.
|
|
96
|
+
- `NEEDS_CUSTOMER_DECISION`: cost limits or mitigation behavior require customer approval.
|
|
97
|
+
|
|
98
|
+
Static repository analysis can produce candidate SQL and exact instrumentation TODOs. Only live field discovery plus SQL validation can justify the `READY` label.
|
package/cli/security.js
CHANGED
|
@@ -48,6 +48,15 @@ async function alertRulesRoute(args, flags) {
|
|
|
48
48
|
if (sub === 'set-sql' || sub === 'edit-sql') {
|
|
49
49
|
return alertRuleSetSql(args.slice(1), flags);
|
|
50
50
|
}
|
|
51
|
+
if (sub === 'versions' || sub === 'history') {
|
|
52
|
+
return alertRuleVersions(args.slice(1), flags);
|
|
53
|
+
}
|
|
54
|
+
if (sub === 'version') {
|
|
55
|
+
return alertRuleVersionShow(args.slice(1), flags);
|
|
56
|
+
}
|
|
57
|
+
if (sub === 'rollback' || sub === 'revert') {
|
|
58
|
+
return alertRuleRollback(args.slice(1), flags);
|
|
59
|
+
}
|
|
51
60
|
if (sub === 'delete' || sub === 'rm' || sub === 'remove') {
|
|
52
61
|
return alertRuleDelete(args.slice(1), flags);
|
|
53
62
|
}
|
|
@@ -464,6 +473,113 @@ async function alertRuleSetSql(args, flags) {
|
|
|
464
473
|
}
|
|
465
474
|
}
|
|
466
475
|
|
|
476
|
+
// List a rule's configuration version history (newest first). Each material update (SQL,
|
|
477
|
+
// instant config, settings), duplicate, and rollback is a version; rollback restores a
|
|
478
|
+
// prior snapshot and is itself recorded as a new version.
|
|
479
|
+
async function alertRuleVersions(args, flags) {
|
|
480
|
+
requireAuth();
|
|
481
|
+
const id = args[0];
|
|
482
|
+
if (!id) {
|
|
483
|
+
ui.error('Usage: securenow alerts rules versions <rule-id> [--limit N] [--page N]');
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
const query = {};
|
|
487
|
+
if (flags.limit) query.limit = flags.limit;
|
|
488
|
+
if (flags.page) query.page = flags.page;
|
|
489
|
+
const s = ui.spinner('Fetching version history');
|
|
490
|
+
let data;
|
|
491
|
+
try {
|
|
492
|
+
data = await api.get(`/alert-rules/${id}/versions`, Object.keys(query).length ? { query } : undefined);
|
|
493
|
+
s.stop(`Found ${data.total ?? (data.versions || []).length} version${(data.total ?? 0) === 1 ? '' : 's'}`);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
s.fail(`Failed to fetch versions: ${err.message}`);
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
if (flags.json) { ui.json(data); return; }
|
|
499
|
+
const rows = data.versions || [];
|
|
500
|
+
if (rows.length === 0) { ui.info('No versions recorded yet.'); return; }
|
|
501
|
+
console.log('');
|
|
502
|
+
ui.info(`Current: v${data.currentVersion}`);
|
|
503
|
+
for (const v of rows) {
|
|
504
|
+
const marker = v.isCurrent ? ui.c.green('→') : ' ';
|
|
505
|
+
const when = v.createdAt ? new Date(v.createdAt).toISOString().replace('T', ' ').slice(0, 19) : '';
|
|
506
|
+
const extra = v.changeType === 'rollback' && v.rolledBackFrom ? ` (from v${v.rolledBackFrom})` : '';
|
|
507
|
+
console.log(`${marker} ${ui.c.bold('v' + v.version).padEnd(14)} ${String(v.changeType + extra).padEnd(22)} ${ui.c.dim(when)} ${v.createdByName || ''}`);
|
|
508
|
+
if (v.reason) console.log(` ${ui.c.dim(ui.truncate(v.reason, 90))}`);
|
|
509
|
+
}
|
|
510
|
+
console.log('');
|
|
511
|
+
ui.info(`Show: securenow alerts rules version ${ui.truncate(id, 12)} <n> | Roll back: securenow alerts rules rollback ${ui.truncate(id, 12)} <n>`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Show one version's full material snapshot (diff-able SQL / instant / settings).
|
|
515
|
+
async function alertRuleVersionShow(args, flags) {
|
|
516
|
+
requireAuth();
|
|
517
|
+
const id = args[0];
|
|
518
|
+
const version = args[1];
|
|
519
|
+
if (!id || !version) {
|
|
520
|
+
ui.error('Usage: securenow alerts rules version <rule-id> <version-number>');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
const s = ui.spinner(`Fetching v${version}`);
|
|
524
|
+
let data;
|
|
525
|
+
try {
|
|
526
|
+
data = await api.get(`/alert-rules/${id}/versions/${version}`);
|
|
527
|
+
s.stop(`Loaded v${version}`);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
s.fail(`Failed: ${err.message}`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (flags.json) { ui.json(data); return; }
|
|
533
|
+
const v = data.version || {};
|
|
534
|
+
const snap = v.snapshot || {};
|
|
535
|
+
console.log('');
|
|
536
|
+
ui.success(`v${v.version}${v.isCurrent ? ' (current)' : ''} — ${v.changeType}`);
|
|
537
|
+
console.log(` When ${v.createdAt || ''}`);
|
|
538
|
+
if (v.createdByName) console.log(` By ${v.createdByName}`);
|
|
539
|
+
if (v.reason) console.log(` Reason ${v.reason}`);
|
|
540
|
+
if (v.rolledBackFrom) console.log(` Restored v${v.rolledBackFrom}`);
|
|
541
|
+
console.log('');
|
|
542
|
+
console.log(` Name ${snap.name || ''}`);
|
|
543
|
+
console.log(` Mode ${snap.mode || ''} / ${snap.executionMode || ''} severity ${snap.severity || '-'}`);
|
|
544
|
+
if (snap.instant && snap.instant.enabled) {
|
|
545
|
+
console.log(` Instant enabled block=${snap.instant.block} (${(snap.instant.conditions || []).length} condition${(snap.instant.conditions || []).length === 1 ? '' : 's'})`);
|
|
546
|
+
}
|
|
547
|
+
if (snap.query && snap.query.sqlQuery) {
|
|
548
|
+
console.log(' SQL:');
|
|
549
|
+
console.log(snap.query.sqlQuery.split('\n').map((l) => ' ' + ui.c.dim(l)).join('\n'));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Roll a rule back to a prior version — restores that version's full snapshot and records it
|
|
554
|
+
// as a new version (append-only history). System (read-only template) rules can't be rolled back.
|
|
555
|
+
async function alertRuleRollback(args, flags) {
|
|
556
|
+
requireAuth();
|
|
557
|
+
const id = args[0];
|
|
558
|
+
const version = args[1] || flags.version;
|
|
559
|
+
if (!id || !version) {
|
|
560
|
+
ui.error('Usage: securenow alerts rules rollback <rule-id> <version-number> [--reason "..."] [--yes]');
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
if (!flags.yes && !flags.force) {
|
|
564
|
+
const ok = await ui.confirm(`Roll back rule ${ui.truncate(id, 12)} to v${version}? Restores that config and creates a new version.`);
|
|
565
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
566
|
+
}
|
|
567
|
+
const body = { version: Number(version) };
|
|
568
|
+
if (flags.reason) body.reason = String(flags.reason);
|
|
569
|
+
const s = ui.spinner(`Rolling back to v${version}`);
|
|
570
|
+
let data;
|
|
571
|
+
try {
|
|
572
|
+
data = await api.post(`/alert-rules/${id}/rollback`, body);
|
|
573
|
+
s.stop(`Rolled back to v${version}`);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
s.fail(`Failed to roll back: ${err.message}`);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
if (flags.json) { ui.json(data); return; }
|
|
579
|
+
ui.success(`Restored from v${data.rolledBackFrom} → now at v${data.newVersion}`);
|
|
580
|
+
ui.info(`History: securenow alerts rules versions ${ui.truncate(id, 12)}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
467
583
|
async function alertRuleDelete(args, flags) {
|
|
468
584
|
requireAuth();
|
|
469
585
|
const id = args[0];
|
|
@@ -967,6 +1083,58 @@ async function blocklistRemove(args, flags) {
|
|
|
967
1083
|
}
|
|
968
1084
|
}
|
|
969
1085
|
|
|
1086
|
+
// Approve a block that the AI prepared but parked pending customer approval
|
|
1087
|
+
// (e.g. the autopilot held it under block_risk_score_below_policy). Enforces the IP.
|
|
1088
|
+
async function blocklistApprove(args, flags) {
|
|
1089
|
+
requireAuth();
|
|
1090
|
+
const id = args[0];
|
|
1091
|
+
if (!id) {
|
|
1092
|
+
ui.error('Usage: securenow blocklist approve <pending-block-id> [--reason <reason>] [--yes]');
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
if (!flags.force && !flags.yes) {
|
|
1096
|
+
const ok = await ui.confirm('Approve this pending block? The IP will be enforced by the firewall.');
|
|
1097
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1098
|
+
}
|
|
1099
|
+
const s = ui.spinner('Approving pending block');
|
|
1100
|
+
try {
|
|
1101
|
+
const body = {};
|
|
1102
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1103
|
+
const data = await api.post(`/blocklist/${id}/approve`, body);
|
|
1104
|
+
s.stop('Pending block approved — IP now enforced');
|
|
1105
|
+
if (flags.json) ui.json(data);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
s.fail('Failed to approve pending block');
|
|
1108
|
+
throw err;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Reject a pending block: the IP is NOT blocked, the entry is removed, and a scoped
|
|
1113
|
+
// no-block decision is recorded so auto-block won't re-block this IP for the same rule.
|
|
1114
|
+
async function blocklistReject(args, flags) {
|
|
1115
|
+
requireAuth();
|
|
1116
|
+
const id = args[0];
|
|
1117
|
+
if (!id) {
|
|
1118
|
+
ui.error('Usage: securenow blocklist reject <pending-block-id> [--reason <reason>] [--yes]');
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
if (!flags.force && !flags.yes) {
|
|
1122
|
+
const ok = await ui.confirm('Reject this pending block? The IP will NOT be blocked; a scoped no-block decision is recorded for the rule that proposed it.');
|
|
1123
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
1124
|
+
}
|
|
1125
|
+
const s = ui.spinner('Rejecting pending block');
|
|
1126
|
+
try {
|
|
1127
|
+
const body = {};
|
|
1128
|
+
if (flags.reason) body.reason = flags.reason;
|
|
1129
|
+
const data = await api.post(`/blocklist/${id}/reject`, body);
|
|
1130
|
+
s.stop('Pending block rejected — IP cleared (scoped no-block decision recorded)');
|
|
1131
|
+
if (flags.json) ui.json(data);
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
s.fail('Failed to reject pending block');
|
|
1134
|
+
throw err;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
970
1138
|
async function blocklistStats(args, flags) {
|
|
971
1139
|
requireAuth();
|
|
972
1140
|
const s = ui.spinner('Fetching blocklist stats');
|
|
@@ -1666,6 +1834,8 @@ module.exports = {
|
|
|
1666
1834
|
blocklistList,
|
|
1667
1835
|
blocklistAdd,
|
|
1668
1836
|
blocklistRemove,
|
|
1837
|
+
blocklistApprove,
|
|
1838
|
+
blocklistReject,
|
|
1669
1839
|
blocklistStats,
|
|
1670
1840
|
revokeRoute,
|
|
1671
1841
|
allowlistList,
|
package/cli/telemetry.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const schemaContract = require('../telemetry-schema.json');
|
|
4
|
+
const { api, requireAuth } = require('./client');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const appConfig = require('../app-config');
|
|
7
|
+
const ui = require('./ui');
|
|
8
|
+
|
|
9
|
+
function parseDurationToMinutes(value) {
|
|
10
|
+
if (value == null || value === '') return 24 * 60;
|
|
11
|
+
const match = String(value).trim().match(/^(\d+)\s*(m|h|d)?$/i);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
const amount = Number.parseInt(match[1], 10);
|
|
14
|
+
const unit = String(match[2] || 'm').toLowerCase();
|
|
15
|
+
if (amount < 1) return null;
|
|
16
|
+
if (unit === 'm') return amount;
|
|
17
|
+
if (unit === 'h') return amount * 60;
|
|
18
|
+
if (unit === 'd') return amount * 24 * 60;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function filteredSchema(section) {
|
|
23
|
+
const selected = String(section || 'all').toLowerCase();
|
|
24
|
+
if (selected === 'all') return schemaContract;
|
|
25
|
+
if (selected === 'logs' || selected === 'traces') {
|
|
26
|
+
return {
|
|
27
|
+
contract: schemaContract.contract,
|
|
28
|
+
schemaVersion: schemaContract.schemaVersion,
|
|
29
|
+
dialect: schemaContract.dialect,
|
|
30
|
+
ruleContract: schemaContract.ruleContract,
|
|
31
|
+
table: schemaContract.tables[selected],
|
|
32
|
+
clientIp: schemaContract.clientIp,
|
|
33
|
+
templates: schemaContract.templates,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (selected === 'events') {
|
|
37
|
+
return {
|
|
38
|
+
contract: schemaContract.contract,
|
|
39
|
+
schemaVersion: schemaContract.schemaVersion,
|
|
40
|
+
dialect: schemaContract.dialect,
|
|
41
|
+
ruleContract: schemaContract.ruleContract,
|
|
42
|
+
storageTable: schemaContract.tables.logs,
|
|
43
|
+
customEvents: schemaContract.customEvents,
|
|
44
|
+
templates: { eventFailuresByIp: schemaContract.templates.eventFailuresByIp },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function schema(args, flags) {
|
|
51
|
+
const section = args[0] || flags.source || 'all';
|
|
52
|
+
const contract = filteredSchema(section);
|
|
53
|
+
if (!contract) {
|
|
54
|
+
ui.error('Unknown schema section. Use all, logs, traces, or events.');
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
ui.json(contract);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log('');
|
|
65
|
+
ui.heading('SecureNow Telemetry & Alert SQL Contract');
|
|
66
|
+
console.log('');
|
|
67
|
+
ui.keyValue([
|
|
68
|
+
['Schema version', schemaContract.schemaVersion],
|
|
69
|
+
['SQL dialect', schemaContract.dialect],
|
|
70
|
+
['Tenant placeholder', schemaContract.ruleContract.tenantPlaceholder],
|
|
71
|
+
['Required result', 'ip column for IP-remediation rules'],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const tables = section === 'all'
|
|
75
|
+
? Object.entries(schemaContract.tables)
|
|
76
|
+
: section === 'events'
|
|
77
|
+
? [['logs', schemaContract.tables.logs]]
|
|
78
|
+
: [[section, schemaContract.tables[section]]];
|
|
79
|
+
console.log('');
|
|
80
|
+
ui.subheading('Tables');
|
|
81
|
+
console.log('');
|
|
82
|
+
ui.table(
|
|
83
|
+
['Signal', 'Table', 'Timestamp', 'Tenant scope'],
|
|
84
|
+
tables.map(([name, table]) => [
|
|
85
|
+
name,
|
|
86
|
+
table.name,
|
|
87
|
+
`${table.timestamp.type} (${table.timestamp.unit})`,
|
|
88
|
+
table.tenantExpression,
|
|
89
|
+
])
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (section === 'all' || section === 'events') {
|
|
93
|
+
console.log('');
|
|
94
|
+
ui.subheading('Custom event mapping');
|
|
95
|
+
console.log('');
|
|
96
|
+
ui.table(
|
|
97
|
+
['SDK property', 'SQL field'],
|
|
98
|
+
Object.entries(schemaContract.customEvents.propertyMapping).map(([property, field]) => [property, field])
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('');
|
|
103
|
+
ui.info('Machine-readable: securenow telemetry schema --json');
|
|
104
|
+
ui.info('Live verification: securenow telemetry fields --app <key> --env production --since 24h --json');
|
|
105
|
+
ui.info('Full guide: node_modules/securenow/TELEMETRY-SQL.md');
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function fields(args, flags) {
|
|
110
|
+
requireAuth();
|
|
111
|
+
const appKey = flags.app || config.getDefaultApp() || appConfig.resolveAppKey();
|
|
112
|
+
if (!appKey) {
|
|
113
|
+
ui.error('No app specified. Use --app <key>, run `securenow app connect`, or set a default app.');
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sinceMinutes = parseDurationToMinutes(flags.since || '24h');
|
|
119
|
+
if (sinceMinutes == null || sinceMinutes > 30 * 24 * 60) {
|
|
120
|
+
ui.error('Invalid --since value. Use a duration such as 30m, 24h, or 7d (maximum 30d).');
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const source = String(flags.source || args[0] || 'all').toLowerCase();
|
|
125
|
+
if (!['all', 'logs', 'traces'].includes(source)) {
|
|
126
|
+
ui.error('Invalid --source. Use all, logs, or traces.');
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const parsedLimit = Number.parseInt(flags.limit || '200', 10);
|
|
131
|
+
if (!Number.isFinite(parsedLimit) || parsedLimit < 1 || parsedLimit > 500) {
|
|
132
|
+
ui.error('--limit must be between 1 and 500.');
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const environment = flags.env || flags.environment || 'production';
|
|
137
|
+
|
|
138
|
+
const spinner = ui.spinner('Discovering observed telemetry fields');
|
|
139
|
+
try {
|
|
140
|
+
const data = await api.get('/telemetry/fields', {
|
|
141
|
+
query: {
|
|
142
|
+
appKeys: appKey,
|
|
143
|
+
environment,
|
|
144
|
+
sinceMinutes,
|
|
145
|
+
source,
|
|
146
|
+
limit: parsedLimit,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
spinner.stop(`Found ${(data.fields || []).length} observed field${(data.fields || []).length === 1 ? '' : 's'}`);
|
|
150
|
+
if (flags.json) {
|
|
151
|
+
ui.json(data);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('');
|
|
156
|
+
ui.heading('Observed Telemetry Fields');
|
|
157
|
+
console.log('');
|
|
158
|
+
ui.keyValue([
|
|
159
|
+
['App', appKey],
|
|
160
|
+
['Environment', (data.environments || [environment]).join(', ')],
|
|
161
|
+
['Window', `${sinceMinutes} minutes`],
|
|
162
|
+
['Source', source],
|
|
163
|
+
]);
|
|
164
|
+
console.log('');
|
|
165
|
+
ui.table(
|
|
166
|
+
['Signal', 'Scope', 'Type', 'Field', 'Occurrences'],
|
|
167
|
+
(data.fields || []).map((field) => [
|
|
168
|
+
field.source,
|
|
169
|
+
field.fieldScope,
|
|
170
|
+
field.dataType,
|
|
171
|
+
field.field,
|
|
172
|
+
String(field.occurrences || 0),
|
|
173
|
+
])
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if ((data.eventTypes || []).length > 0) {
|
|
177
|
+
console.log('');
|
|
178
|
+
ui.subheading('Observed custom event types');
|
|
179
|
+
console.log('');
|
|
180
|
+
ui.table(
|
|
181
|
+
['Event type', 'Occurrences'],
|
|
182
|
+
data.eventTypes.map((event) => [event.eventType, String(event.occurrences || 0)])
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
console.log('');
|
|
186
|
+
ui.info(data.note || 'Absence in this window does not prove a field can never be emitted.');
|
|
187
|
+
console.log('');
|
|
188
|
+
} catch (error) {
|
|
189
|
+
spinner.fail('Failed to discover telemetry fields');
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
schema,
|
|
196
|
+
fields,
|
|
197
|
+
parseDurationToMinutes,
|
|
198
|
+
filteredSchema,
|
|
199
|
+
};
|