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 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
- ```bash
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
@@ -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.
@@ -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
- notifications: {
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.9.0",
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.218.0",
169
- "@opentelemetry/auto-instrumentations-node": "0.76.0",
170
- "@opentelemetry/core": "2.7.1",
171
- "@opentelemetry/exporter-logs-otlp-http": "0.218.0",
172
- "@opentelemetry/exporter-trace-otlp-http": "0.218.0",
173
- "@opentelemetry/instrumentation": "0.218.0",
174
- "@opentelemetry/instrumentation-document-load": "0.63.0",
175
- "@opentelemetry/instrumentation-fetch": "0.218.0",
176
- "@opentelemetry/instrumentation-http": "0.218.0",
177
- "@opentelemetry/instrumentation-mongodb": "0.71.0",
178
- "@opentelemetry/instrumentation-user-interaction": "0.62.0",
179
- "@opentelemetry/instrumentation-xml-http-request": "0.218.0",
180
- "@opentelemetry/resources": "2.7.1",
181
- "@opentelemetry/sdk-logs": "0.218.0",
182
- "@opentelemetry/sdk-metrics": "2.7.1",
183
- "@opentelemetry/sdk-node": "0.218.0",
184
- "@opentelemetry/sdk-trace-base": "2.7.1",
185
- "@opentelemetry/sdk-trace-web": "2.7.1",
186
- "@opentelemetry/semantic-conventions": "1.41.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.2"
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,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./telemetry-schema.json');
@@ -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
+ }