securenow 8.9.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/telemetry.js +199 -0
- package/cli.js +43 -5
- 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/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
|
+
};
|
package/cli.js
CHANGED
|
@@ -188,16 +188,54 @@ const COMMANDS = {
|
|
|
188
188
|
},
|
|
189
189
|
defaultSub: 'list',
|
|
190
190
|
},
|
|
191
|
-
logs: {
|
|
191
|
+
logs: {
|
|
192
192
|
desc: 'View application logs',
|
|
193
193
|
usage: 'securenow logs [options]',
|
|
194
194
|
sub: {
|
|
195
195
|
list: { desc: 'List recent logs', flags: { app: 'App key', env: 'Environment (production, staging, preview, local, or all)', environment: 'Alias for --env', limit: 'Max results (default 200)', since: 'Time window (e.g. 30m, 6h, 2d)', minutes: 'Time window in minutes (alias for --since Nm)', level: 'Filter by level (error, warn, info, debug)', start: 'Start time (ISO 8601)', end: 'End time (ISO 8601)' }, run: (a, f) => require('./cli/monitor').logsList(a, f) },
|
|
196
196
|
trace: { desc: 'Show logs for a trace', usage: 'securenow logs trace <traceId>', flags: { app: 'App key', env: 'Environment', environment: 'Alias for --env' }, run: (a, f) => require('./cli/monitor').logsTrace(a, f) },
|
|
197
197
|
},
|
|
198
|
-
defaultSub: 'list',
|
|
199
|
-
},
|
|
200
|
-
|
|
198
|
+
defaultSub: 'list',
|
|
199
|
+
},
|
|
200
|
+
telemetry: {
|
|
201
|
+
desc: 'Inspect the alert-SQL telemetry contract and fields observed for an app',
|
|
202
|
+
usage: 'securenow telemetry <schema|fields> [options]',
|
|
203
|
+
flags: {
|
|
204
|
+
app: 'With fields: application key (defaults to connected/default app)',
|
|
205
|
+
env: 'With fields: environment or all (default: production)',
|
|
206
|
+
since: 'With fields: observation window such as 30m, 24h, or 7d (maximum 30d)',
|
|
207
|
+
source: 'Schema section or live signal source',
|
|
208
|
+
limit: 'With fields: maximum fields per signal (1-500, default 200)',
|
|
209
|
+
json: 'Machine-readable output',
|
|
210
|
+
},
|
|
211
|
+
sub: {
|
|
212
|
+
schema: {
|
|
213
|
+
desc: 'Print the packaged ClickHouse tables, fields, mappings, and SQL requirements',
|
|
214
|
+
usage: 'securenow telemetry schema [all|logs|traces|events] [--json]',
|
|
215
|
+
flags: {
|
|
216
|
+
source: 'Schema section: all, logs, traces, or events',
|
|
217
|
+
json: 'Output the machine-readable contract as JSON',
|
|
218
|
+
},
|
|
219
|
+
run: (a, f) => require('./cli/telemetry').schema(a, f),
|
|
220
|
+
},
|
|
221
|
+
fields: {
|
|
222
|
+
desc: 'Discover fields and custom event types actually emitted by an app',
|
|
223
|
+
usage: 'securenow telemetry fields [--app <key>] [--env production] [--since 24h] [--source all] [--limit 200] [--json]',
|
|
224
|
+
flags: {
|
|
225
|
+
app: 'Application key (defaults to connected/default app)',
|
|
226
|
+
env: 'Environment, comma-separated environments, or all (default: production)',
|
|
227
|
+
environment: 'Alias for --env',
|
|
228
|
+
since: 'Observation window such as 30m, 24h, or 7d (maximum 30d)',
|
|
229
|
+
source: 'Signal source: all, logs, or traces',
|
|
230
|
+
limit: 'Maximum fields per signal (1-500, default 200)',
|
|
231
|
+
json: 'Output JSON for agents and scripts',
|
|
232
|
+
},
|
|
233
|
+
run: (a, f) => require('./cli/telemetry').fields(a, f),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
defaultSub: 'schema',
|
|
237
|
+
},
|
|
238
|
+
notifications: {
|
|
201
239
|
desc: 'Manage notifications',
|
|
202
240
|
usage: 'securenow notifications <subcommand> [options]',
|
|
203
241
|
sub: {
|
|
@@ -765,7 +803,7 @@ function showHelp(commandName) {
|
|
|
765
803
|
'Investigate': ['ip', 'forensics'],
|
|
766
804
|
'Firewall': ['firewall'],
|
|
767
805
|
'Remediation': ['automation', 'ratelimit', 'challenge', 'blocklist', 'revoke', 'allowlist', 'trusted'],
|
|
768
|
-
'Telemetry': ['log', 'event', 'test-span'],
|
|
806
|
+
'Telemetry': ['telemetry', 'log', 'event', 'test-span'],
|
|
769
807
|
'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
|
|
770
808
|
'Settings': ['instances', 'config', 'version'],
|
|
771
809
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securenow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.10.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "register.js",
|
|
@@ -50,10 +50,14 @@
|
|
|
50
50
|
"types": "./tracing.d.ts",
|
|
51
51
|
"default": "./tracing.js"
|
|
52
52
|
},
|
|
53
|
-
"./events": {
|
|
54
|
-
"types": "./events.d.ts",
|
|
55
|
-
"default": "./events.js"
|
|
56
|
-
},
|
|
53
|
+
"./events": {
|
|
54
|
+
"types": "./events.d.ts",
|
|
55
|
+
"default": "./events.js"
|
|
56
|
+
},
|
|
57
|
+
"./telemetry-schema": {
|
|
58
|
+
"types": "./telemetry-schema.d.ts",
|
|
59
|
+
"default": "./telemetry-schema.js"
|
|
60
|
+
},
|
|
57
61
|
"./sessions": {
|
|
58
62
|
"types": "./sessions.d.ts",
|
|
59
63
|
"default": "./sessions.js"
|
|
@@ -123,8 +127,11 @@
|
|
|
123
127
|
"register.d.ts",
|
|
124
128
|
"tracing.js",
|
|
125
129
|
"tracing.d.ts",
|
|
126
|
-
"events.js",
|
|
127
|
-
"events.d.ts",
|
|
130
|
+
"events.js",
|
|
131
|
+
"events.d.ts",
|
|
132
|
+
"telemetry-schema.js",
|
|
133
|
+
"telemetry-schema.d.ts",
|
|
134
|
+
"telemetry-schema.json",
|
|
128
135
|
"sessions.js",
|
|
129
136
|
"sessions.d.ts",
|
|
130
137
|
"console-instrumentation.js",
|
|
@@ -159,34 +166,35 @@
|
|
|
159
166
|
"web-vite.mjs",
|
|
160
167
|
"app-config.js",
|
|
161
168
|
"README.md",
|
|
162
|
-
"NPM_README.md",
|
|
169
|
+
"NPM_README.md",
|
|
170
|
+
"TELEMETRY-SQL.md",
|
|
163
171
|
"SKILL-CLI.md",
|
|
164
172
|
"SKILL-API.md"
|
|
165
173
|
],
|
|
166
174
|
"dependencies": {
|
|
167
175
|
"@opentelemetry/api": ">=1.9.0 <1.10.0",
|
|
168
|
-
"@opentelemetry/api-logs": "0.
|
|
169
|
-
"@opentelemetry/auto-instrumentations-node": "0.
|
|
170
|
-
"@opentelemetry/core": "2.
|
|
171
|
-
"@opentelemetry/exporter-logs-otlp-http": "0.
|
|
172
|
-
"@opentelemetry/exporter-trace-otlp-http": "0.
|
|
173
|
-
"@opentelemetry/instrumentation": "0.
|
|
174
|
-
"@opentelemetry/instrumentation-document-load": "0.
|
|
175
|
-
"@opentelemetry/instrumentation-fetch": "0.
|
|
176
|
-
"@opentelemetry/instrumentation-http": "0.
|
|
177
|
-
"@opentelemetry/instrumentation-mongodb": "0.
|
|
178
|
-
"@opentelemetry/instrumentation-user-interaction": "0.
|
|
179
|
-
"@opentelemetry/instrumentation-xml-http-request": "0.
|
|
180
|
-
"@opentelemetry/resources": "2.
|
|
181
|
-
"@opentelemetry/sdk-logs": "0.
|
|
182
|
-
"@opentelemetry/sdk-metrics": "2.
|
|
183
|
-
"@opentelemetry/sdk-node": "0.
|
|
184
|
-
"@opentelemetry/sdk-trace-base": "2.
|
|
185
|
-
"@opentelemetry/sdk-trace-web": "2.
|
|
186
|
-
"@opentelemetry/semantic-conventions": "1.
|
|
176
|
+
"@opentelemetry/api-logs": "0.220.0",
|
|
177
|
+
"@opentelemetry/auto-instrumentations-node": "0.78.0",
|
|
178
|
+
"@opentelemetry/core": "2.9.0",
|
|
179
|
+
"@opentelemetry/exporter-logs-otlp-http": "0.220.0",
|
|
180
|
+
"@opentelemetry/exporter-trace-otlp-http": "0.220.0",
|
|
181
|
+
"@opentelemetry/instrumentation": "0.220.0",
|
|
182
|
+
"@opentelemetry/instrumentation-document-load": "0.65.0",
|
|
183
|
+
"@opentelemetry/instrumentation-fetch": "0.220.0",
|
|
184
|
+
"@opentelemetry/instrumentation-http": "0.220.0",
|
|
185
|
+
"@opentelemetry/instrumentation-mongodb": "0.73.0",
|
|
186
|
+
"@opentelemetry/instrumentation-user-interaction": "0.64.0",
|
|
187
|
+
"@opentelemetry/instrumentation-xml-http-request": "0.220.0",
|
|
188
|
+
"@opentelemetry/resources": "2.9.0",
|
|
189
|
+
"@opentelemetry/sdk-logs": "0.220.0",
|
|
190
|
+
"@opentelemetry/sdk-metrics": "2.9.0",
|
|
191
|
+
"@opentelemetry/sdk-node": "0.220.0",
|
|
192
|
+
"@opentelemetry/sdk-trace-base": "2.9.0",
|
|
193
|
+
"@opentelemetry/sdk-trace-web": "2.9.0",
|
|
194
|
+
"@opentelemetry/semantic-conventions": "1.43.0"
|
|
187
195
|
},
|
|
188
196
|
"optionalDependencies": {
|
|
189
|
-
"@vercel/otel": "2.1.
|
|
197
|
+
"@vercel/otel": "2.1.3"
|
|
190
198
|
},
|
|
191
199
|
"sideEffects": true,
|
|
192
200
|
"license": "ISC"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SecureNowTelemetrySchema {
|
|
2
|
+
contract: string;
|
|
3
|
+
schemaVersion: string;
|
|
4
|
+
dialect: 'ClickHouse';
|
|
5
|
+
purpose: string;
|
|
6
|
+
ruleContract: Record<string, unknown>;
|
|
7
|
+
tables: Record<string, unknown>;
|
|
8
|
+
clientIp: Record<string, unknown>;
|
|
9
|
+
customEvents: Record<string, unknown>;
|
|
10
|
+
automaticTraceFields: Record<string, string[]>;
|
|
11
|
+
templates: Record<string, string>;
|
|
12
|
+
authoringWorkflow: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare const schema: SecureNowTelemetrySchema;
|
|
16
|
+
export = schema;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": "securenow.telemetry.alert-sql",
|
|
3
|
+
"schemaVersion": "1.0.0",
|
|
4
|
+
"dialect": "ClickHouse",
|
|
5
|
+
"purpose": "Canonical local contract for agents and humans authoring SecureNow alert rules.",
|
|
6
|
+
"ruleContract": {
|
|
7
|
+
"tenantPlaceholder": "__USER_APP_KEYS__",
|
|
8
|
+
"requiredResultColumns": [
|
|
9
|
+
{
|
|
10
|
+
"name": "ip",
|
|
11
|
+
"type": "String",
|
|
12
|
+
"reason": "Per-IP grouping and IP-based remediation use this column. Keep empty IPs out of the final result."
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"requirements": [
|
|
16
|
+
"Query only documented telemetry tables and fields.",
|
|
17
|
+
"Keep __USER_APP_KEYS__ in every customer-scoped query.",
|
|
18
|
+
"Use the tenant expression documented for the selected table.",
|
|
19
|
+
"Return an ip column and exclude empty IP values for IP-remediation rules.",
|
|
20
|
+
"Bound every query with a time window.",
|
|
21
|
+
"Validate candidate SQL before creating or updating a rule.",
|
|
22
|
+
"Treat fields absent from live field discovery as unverified until instrumentation emits them."
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"tables": {
|
|
26
|
+
"logs": {
|
|
27
|
+
"name": "signoz_logs.distributed_logs_v2",
|
|
28
|
+
"purpose": "OpenTelemetry logs and SecureNow custom security events.",
|
|
29
|
+
"timestamp": {
|
|
30
|
+
"column": "timestamp",
|
|
31
|
+
"type": "UInt64",
|
|
32
|
+
"unit": "nanoseconds",
|
|
33
|
+
"timeFilterExample": "timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)"
|
|
34
|
+
},
|
|
35
|
+
"tenantExpression": "resources_string['service.name'] IN (__USER_APP_KEYS__)",
|
|
36
|
+
"columns": {
|
|
37
|
+
"timestamp": "UInt64 nanoseconds",
|
|
38
|
+
"observed_timestamp": "UInt64 nanoseconds",
|
|
39
|
+
"id": "String",
|
|
40
|
+
"trace_id": "String",
|
|
41
|
+
"span_id": "String",
|
|
42
|
+
"severity_text": "LowCardinality(String)",
|
|
43
|
+
"severity_number": "UInt8",
|
|
44
|
+
"body": "String",
|
|
45
|
+
"attributes_string": "Map(String, String)",
|
|
46
|
+
"attributes_number": "Map(String, Float64)",
|
|
47
|
+
"attributes_bool": "Map(String, Bool)",
|
|
48
|
+
"resources_string": "Map(String, String)",
|
|
49
|
+
"scope_name": "String",
|
|
50
|
+
"scope_version": "String"
|
|
51
|
+
},
|
|
52
|
+
"mapAccess": {
|
|
53
|
+
"string": "attributes_string['field.name']",
|
|
54
|
+
"number": "attributes_number['field.name']",
|
|
55
|
+
"boolean": "attributes_bool['field.name']",
|
|
56
|
+
"resource": "resources_string['field.name']"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"traces": {
|
|
60
|
+
"name": "signoz_traces.distributed_signoz_index_v3",
|
|
61
|
+
"purpose": "OpenTelemetry spans including automatically captured HTTP request telemetry.",
|
|
62
|
+
"timestamp": {
|
|
63
|
+
"column": "timestamp",
|
|
64
|
+
"type": "DateTime64(9)",
|
|
65
|
+
"unit": "nanoseconds precision",
|
|
66
|
+
"timeFilterExample": "timestamp >= now64(9) - INTERVAL 15 MINUTE"
|
|
67
|
+
},
|
|
68
|
+
"tenantExpression": "`resource_string_service$$name` IN (__USER_APP_KEYS__)",
|
|
69
|
+
"columns": {
|
|
70
|
+
"timestamp": "DateTime64(9)",
|
|
71
|
+
"trace_id": "FixedString(32)",
|
|
72
|
+
"span_id": "String",
|
|
73
|
+
"parent_span_id": "String",
|
|
74
|
+
"name": "LowCardinality(String)",
|
|
75
|
+
"kind": "Int8 (1 internal, 2 server, 3 client, 4 producer, 5 consumer)",
|
|
76
|
+
"duration_nano": "UInt64",
|
|
77
|
+
"status_code": "Int16",
|
|
78
|
+
"status_message": "String",
|
|
79
|
+
"attributes_string": "Map(String, String)",
|
|
80
|
+
"attributes_number": "Map(String, Float64)",
|
|
81
|
+
"attributes_bool": "Map(String, Bool)",
|
|
82
|
+
"resources_string": "Map(String, String)",
|
|
83
|
+
"response_status_code": "LowCardinality(String)",
|
|
84
|
+
"http_url": "LowCardinality(String)",
|
|
85
|
+
"http_method": "LowCardinality(String)",
|
|
86
|
+
"http_host": "LowCardinality(String)",
|
|
87
|
+
"has_error": "Bool",
|
|
88
|
+
"resource_string_service$$name": "LowCardinality(String)",
|
|
89
|
+
"attribute_string_http$$route": "LowCardinality(String)"
|
|
90
|
+
},
|
|
91
|
+
"mapAccess": {
|
|
92
|
+
"string": "attributes_string['field.name']",
|
|
93
|
+
"number": "attributes_number['field.name']",
|
|
94
|
+
"boolean": "attributes_bool['field.name']",
|
|
95
|
+
"resource": "resources_string['field.name']"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"clientIp": {
|
|
100
|
+
"preferredField": "http.client_ip",
|
|
101
|
+
"traceExpression": "coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], ''), nullIf(attributes_string['client.address'], ''), nullIf(attributes_string['net.sock.peer.addr'], ''))",
|
|
102
|
+
"eventExpression": "attributes_string['http.client_ip']",
|
|
103
|
+
"trustNote": "Confirm trusted-proxy configuration before using forwarded IPs for blocking."
|
|
104
|
+
},
|
|
105
|
+
"customEvents": {
|
|
106
|
+
"sdkImport": "securenow/events",
|
|
107
|
+
"signature": "track(type: string, props?: TrackProps): void",
|
|
108
|
+
"storageTable": "logs",
|
|
109
|
+
"propertyMapping": {
|
|
110
|
+
"type": "attributes_string['event.type']",
|
|
111
|
+
"userId": "attributes_string['enduser.id']",
|
|
112
|
+
"sessionId": "attributes_string['session.id']",
|
|
113
|
+
"ip": "attributes_string['http.client_ip']",
|
|
114
|
+
"userAgent": "attributes_string['http.user_agent']",
|
|
115
|
+
"severity": "severity_text",
|
|
116
|
+
"attributes.string": "attributes_string['<custom-key>']",
|
|
117
|
+
"attributes.number": "attributes_number['<custom-key>']",
|
|
118
|
+
"attributes.boolean": "attributes_bool['<custom-key>']"
|
|
119
|
+
},
|
|
120
|
+
"recommendedAuthenticationEvents": [
|
|
121
|
+
"auth.login.failure",
|
|
122
|
+
"auth.login.success",
|
|
123
|
+
"auth.otp.send.requested",
|
|
124
|
+
"auth.otp.send.result",
|
|
125
|
+
"auth.otp.verify.failure",
|
|
126
|
+
"auth.otp.verify.success",
|
|
127
|
+
"auth.password_reset.requested",
|
|
128
|
+
"auth.password_reset.completed",
|
|
129
|
+
"auth.mfa.challenge.failure",
|
|
130
|
+
"auth.mfa.challenge.success",
|
|
131
|
+
"auth.session.revoked"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"automaticTraceFields": {
|
|
135
|
+
"http": [
|
|
136
|
+
"http.method",
|
|
137
|
+
"http.route",
|
|
138
|
+
"http.target",
|
|
139
|
+
"http.url",
|
|
140
|
+
"http.status_code",
|
|
141
|
+
"http.host",
|
|
142
|
+
"http.scheme",
|
|
143
|
+
"http.user_agent"
|
|
144
|
+
],
|
|
145
|
+
"clientAndProxy": [
|
|
146
|
+
"http.client_ip",
|
|
147
|
+
"http.client_ip.source",
|
|
148
|
+
"http.socket_ip",
|
|
149
|
+
"http.forwarded_for",
|
|
150
|
+
"http.real_ip",
|
|
151
|
+
"http.cf.connecting_ip",
|
|
152
|
+
"http.proxy.trusted"
|
|
153
|
+
],
|
|
154
|
+
"securityPresenceOnly": [
|
|
155
|
+
"http.security.auth_present",
|
|
156
|
+
"http.security.cookies_present",
|
|
157
|
+
"http.security.csrf_token_present"
|
|
158
|
+
],
|
|
159
|
+
"optionalBodyCapture": [
|
|
160
|
+
"http.request.body",
|
|
161
|
+
"http.request.body.type",
|
|
162
|
+
"http.request.body.size",
|
|
163
|
+
"http.request.body.parse_error"
|
|
164
|
+
],
|
|
165
|
+
"resources": [
|
|
166
|
+
"service.name",
|
|
167
|
+
"service.instance.id",
|
|
168
|
+
"service.version",
|
|
169
|
+
"deployment.environment",
|
|
170
|
+
"telemetry.sdk.name",
|
|
171
|
+
"telemetry.sdk.language",
|
|
172
|
+
"telemetry.sdk.version"
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
"templates": {
|
|
176
|
+
"traceBurstByIp": "WITH coalesce(nullIf(attributes_string['http.client_ip'], ''), nullIf(attributes_string['net.peer.ip'], ''), nullIf(attributes_string['network.peer.address'], ''), nullIf(attributes_string['client.address'], ''), nullIf(attributes_string['net.sock.peer.addr'], '')) AS client_ip\nSELECT client_ip AS ip, count() AS hits\nFROM signoz_traces.distributed_signoz_index_v3\nWHERE `resource_string_service$$name` IN (__USER_APP_KEYS__)\n AND timestamp >= now64(9) - INTERVAL 15 MINUTE\n AND attributes_string['http.target'] = '/replace-with-real-route'\nGROUP BY ip\nHAVING ip != '' AND hits >= 20",
|
|
177
|
+
"eventFailuresByIp": "SELECT attributes_string['http.client_ip'] AS ip, count() AS failures\nFROM signoz_logs.distributed_logs_v2\nWHERE resources_string['service.name'] IN (__USER_APP_KEYS__)\n AND timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)\n AND attributes_string['event.type'] = 'replace.with.verified.event'\nGROUP BY ip\nHAVING ip != '' AND failures >= 10"
|
|
178
|
+
},
|
|
179
|
+
"authoringWorkflow": [
|
|
180
|
+
"npx securenow telemetry schema --json",
|
|
181
|
+
"npx securenow telemetry fields --app <key> --env production --since 24h --json",
|
|
182
|
+
"npx securenow alerts rules --json",
|
|
183
|
+
"npx securenow alerts rules validate --sql @rule.sql --app <key>",
|
|
184
|
+
"npx securenow alerts rules create --name <name> --sql @rule.sql --apps <key>",
|
|
185
|
+
"npx securenow alerts rules test <rule-id> --mode dry_run --wait",
|
|
186
|
+
"npx securenow alerts rules promote <rule-id>"
|
|
187
|
+
]
|
|
188
|
+
}
|