securenow 8.9.0 → 8.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL-CLI.md CHANGED
@@ -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.
package/cli/agent.js ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ // `securenow agent` — the SOC runbook prompts that SecureNow's retired built-in
4
+ // AI Security Agent used to run on a cron. Print them here so customers can run
5
+ // the same work externally with their own harness (Claude, Hermes, OpenClaw, or
6
+ // any MCP-capable agent), e.g.:
7
+ //
8
+ // claude -p "$(securenow agent prompt patrol --confirm-writes)"
9
+ //
10
+ // The same prompts are published over MCP via prompts/list (soc_patrol,
11
+ // work_alert_review, work_human_actions, ...).
12
+
13
+ const ui = require('./ui');
14
+ const { PROMPTS, promptMessages } = require('../mcp/catalog.js');
15
+
16
+ const PROMPT_ALIASES = {
17
+ patrol: 'soc_patrol',
18
+ soc_patrol: 'soc_patrol',
19
+ 'alert-review': 'work_alert_review',
20
+ work_alert_review: 'work_alert_review',
21
+ 'human-queue': 'work_human_actions',
22
+ human: 'work_human_actions',
23
+ work_human_actions: 'work_human_actions',
24
+ };
25
+
26
+ function promptText(name, args) {
27
+ const messages = promptMessages(name, args);
28
+ return messages
29
+ .map((message) => message?.content?.text || '')
30
+ .filter(Boolean)
31
+ .join('\n\n');
32
+ }
33
+
34
+ async function prompts(args, flags = {}) {
35
+ const rows = PROMPTS.map((p) => ({ name: p.name, title: p.title, description: p.description }));
36
+ if (flags.json) return ui.json({ prompts: rows, aliases: PROMPT_ALIASES });
37
+ ui.heading('SecureNow agent runbook prompts');
38
+ for (const row of rows) {
39
+ console.log(`- ${row.name}: ${row.description}`);
40
+ }
41
+ console.log('');
42
+ console.log('Print one with: securenow agent prompt <patrol|alert-review|human-queue|full name>');
43
+ console.log('Scheduled example: claude -p "$(securenow agent prompt patrol --confirm-writes)"');
44
+ }
45
+
46
+ async function prompt(args, flags = {}) {
47
+ const ref = String(args[0] || 'patrol').toLowerCase();
48
+ const name = PROMPT_ALIASES[ref] || ref;
49
+ if (!PROMPTS.some((p) => p.name === name)) {
50
+ throw new Error(`Unknown prompt "${args[0]}". Run \`securenow agent prompts\` to list them.`);
51
+ }
52
+ const promptArgs = {
53
+ confirmWrites: Boolean(flags['confirm-writes'] || flags.confirmWrites || flags.yes),
54
+ };
55
+ if (flags.limit) promptArgs.limit = flags.limit;
56
+ if (flags.search) promptArgs.search = flags.search;
57
+ if (flags['max-alert-review-groups']) promptArgs.maxAlertReviewGroups = flags['max-alert-review-groups'];
58
+ if (flags['max-human-rows']) promptArgs.maxHumanRows = flags['max-human-rows'];
59
+
60
+ const text = promptText(name, promptArgs);
61
+ if (flags.json) return ui.json({ name, prompt: text });
62
+ console.log(text);
63
+ }
64
+
65
+ module.exports = { prompt, prompts };
@@ -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
@@ -197,6 +197,44 @@ const COMMANDS = {
197
197
  },
198
198
  defaultSub: 'list',
199
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
+ },
200
238
  notifications: {
201
239
  desc: 'Manage notifications',
202
240
  usage: 'securenow notifications <subcommand> [options]',
@@ -250,6 +288,23 @@ const COMMANDS = {
250
288
  },
251
289
  defaultSub: 'list',
252
290
  },
291
+ agent: {
292
+ desc: 'SOC runbook prompts for running the SecureNow agent work with your own harness (Claude, Hermes, OpenClaw, ...)',
293
+ usage: 'securenow agent <prompt|prompts> [patrol|alert-review|human-queue] [options]',
294
+ flags: {
295
+ json: 'Output as JSON',
296
+ 'confirm-writes': 'Emit the prompt variant that allows the external agent to execute decisions',
297
+ limit: 'Row/group budget baked into the prompt',
298
+ search: 'Optional search filter baked into the prompt',
299
+ 'max-alert-review-groups': 'SOC patrol: max alert-review root-cause groups per pass',
300
+ 'max-human-rows': 'SOC patrol: max Requires Human rows per pass',
301
+ },
302
+ sub: {
303
+ prompt: { desc: 'Print one runbook prompt (pipe it into your harness or schedule it on a cron)', usage: 'securenow agent prompt <patrol|alert-review|human-queue> [--confirm-writes]', run: (a, f) => require('./cli/agent').prompt(a, f) },
304
+ prompts: { desc: 'List the available runbook prompts (same catalog as MCP prompts/list)', run: (a, f) => require('./cli/agent').prompts(a, f) },
305
+ },
306
+ defaultSub: 'prompts',
307
+ },
253
308
  alerts: {
254
309
  desc: 'Manage alerting',
255
310
  usage: 'securenow alerts <subcommand> [options]',
@@ -765,7 +820,7 @@ function showHelp(commandName) {
765
820
  'Investigate': ['ip', 'forensics'],
766
821
  'Firewall': ['firewall'],
767
822
  'Remediation': ['automation', 'ratelimit', 'challenge', 'blocklist', 'revoke', 'allowlist', 'trusted'],
768
- 'Telemetry': ['log', 'event', 'test-span'],
823
+ 'Telemetry': ['telemetry', 'log', 'event', 'test-span'],
769
824
  'Utilities': ['redact', 'cidr', 'doctor', 'env', 'mcp'],
770
825
  'Settings': ['instances', 'config', 'version'],
771
826
  };