securenow 8.11.2 → 8.12.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/README.md +7 -8
- package/SKILL-API.md +0 -1
- package/SKILL-CLI.md +624 -632
- package/TELEMETRY-SQL.md +6 -1
- package/app-config.js +10 -7
- package/cidr.js +83 -83
- package/cli/apps.js +36 -8
- package/cli/client.js +102 -102
- package/cli/diagnostics.js +5 -1
- package/cli/firewall.js +141 -141
- package/cli/fp.js +638 -638
- package/cli/monitor.js +398 -398
- package/cli/run.js +132 -132
- package/cli/security.js +1866 -1856
- package/cli/ui.js +374 -374
- package/cli.js +923 -922
- package/firewall-cloud.js +202 -202
- package/firewall-iptables.js +139 -139
- package/firewall-only.js +46 -46
- package/firewall-tcp.js +74 -74
- package/firewall.js +1470 -1470
- package/mcp/catalog.js +2358 -2346
- package/nextjs-auto-capture.d.ts +29 -29
- package/nextjs-auto-capture.js +273 -273
- package/nextjs-middleware.d.ts +57 -57
- package/nextjs-middleware.js +267 -267
- package/nextjs-webpack-config.js +79 -79
- package/nextjs-wrapper.d.ts +95 -95
- package/nextjs-wrapper.js +233 -233
- package/nextjs.js +0 -9
- package/nuxt-server-plugin.mjs +0 -11
- package/nuxt.mjs +74 -74
- package/package.json +1 -2
- package/register.d.ts +42 -42
- package/register.js +44 -44
- package/resolve-ip.js +13 -13
- package/telemetry-schema.json +3 -1
- package/tracing.js +847 -853
- package/web-vite.mjs +0 -76
- package/free-trial-banner.js +0 -175
package/TELEMETRY-SQL.md
CHANGED
|
@@ -81,10 +81,15 @@ Event properties are stored in the logs table:
|
|
|
81
81
|
| `userId` | `attributes_string['enduser.id']` |
|
|
82
82
|
| `sessionId` | `attributes_string['session.id']` |
|
|
83
83
|
| `ip` | `attributes_string['http.client_ip']` |
|
|
84
|
-
| `userAgent` | `attributes_string['
|
|
84
|
+
| `userAgent` | `attributes_string['user_agent.original']` (HTTP spans use `http.user_agent`) |
|
|
85
85
|
| String custom attribute | `attributes_string['<key>']` |
|
|
86
86
|
| Numeric custom attribute | `attributes_number['<key>']` |
|
|
87
87
|
| Boolean custom attribute | `attributes_bool['<key>']` |
|
|
88
|
+
| ASN of `ip` (when the instance has ASN enrichment) | `attributes_string['client.asn']`, `attributes_string['client.as_org']` |
|
|
89
|
+
|
|
90
|
+
Custom attribute keys that start with a reserved prefix — `event.`, `enduser.`, `session.`, `http.`, `service.`, `securenow.`, `client.` — are stored as `attr.<key>`. Name custom attributes without those prefixes (`auth_method`, `device_hash`, …). Events carry the timestamp the sender supplied; there is no separate ingest time and no event ID.
|
|
91
|
+
|
|
92
|
+
The logs `timestamp` is UInt64 nanoseconds: filter with `timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)`. `timestamp >= now() - INTERVAL …` is accepted by ClickHouse but always true on this table, so the rule would scan the whole retention period — the API rejects it on rule create/update.
|
|
88
93
|
|
|
89
94
|
Never send raw passwords, tokens, OTPs, session secrets, email addresses, phone numbers, or other unnecessary personal data. Hash destinations when correlation is required.
|
|
90
95
|
|
package/app-config.js
CHANGED
|
@@ -17,9 +17,13 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const os = require('os');
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Shared SecureNow Cloud ingestion gateway (production collector for every
|
|
21
|
+
// paying account and for the 14-day trial).
|
|
22
|
+
const SECURENOW_CLOUD_INGEST = 'https://ingest.securenow.ai';
|
|
23
|
+
// Deprecated alias kept for consumers of securenow/app-config.
|
|
24
|
+
const FREE_TRIAL_INSTANCE = SECURENOW_CLOUD_INGEST;
|
|
21
25
|
const DEFAULT_API_URL = 'https://api.securenow.ai';
|
|
22
|
-
const DEFAULT_FIREWALL_API_URL =
|
|
26
|
+
const DEFAULT_FIREWALL_API_URL = SECURENOW_CLOUD_INGEST;
|
|
23
27
|
const LEGACY_SECURENOW_GATEWAY = 'https://api.securenow.ai/api/otlp';
|
|
24
28
|
const CONFIG_SCHEMA_VERSION = 2;
|
|
25
29
|
const CREDENTIAL_FILE_ENVIRONMENTS = Object.freeze([
|
|
@@ -58,7 +62,6 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
58
62
|
noUuid: null,
|
|
59
63
|
strict: false,
|
|
60
64
|
testSpan: false,
|
|
61
|
-
hideBanner: false,
|
|
62
65
|
},
|
|
63
66
|
firewall: {
|
|
64
67
|
enabled: true,
|
|
@@ -112,7 +115,6 @@ const CONFIG_EXPLANATIONS = Object.freeze({
|
|
|
112
115
|
'config.runtime.noUuid': 'null means auto: true when app.key is present. Set true/false only for advanced routing needs.',
|
|
113
116
|
'config.runtime.strict': 'If true, PM2/cluster workers exit when no app identity is resolvable.',
|
|
114
117
|
'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
|
|
115
|
-
'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
|
|
116
118
|
'config.firewall.enabled': 'Deprecated local hint. Runtime firewall enforcement is controlled by the SecureNow dashboard/API app environment toggle; the SDK starts sync whenever apiKey is present.',
|
|
117
119
|
'config.firewall.apiUrl': 'Optional SecureNow firewall control-plane base URL. Leave unset/default so hosted SDKs sync through the SecureNow ingest gateway.',
|
|
118
120
|
'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version/ETag checks.',
|
|
@@ -440,13 +442,13 @@ function normalizeInstanceEndpoint(value) {
|
|
|
440
442
|
if (value === undefined || value === null || value === '') return value;
|
|
441
443
|
const endpoint = String(value).trim().replace(/\/$/, '');
|
|
442
444
|
if (!endpoint) return endpoint;
|
|
443
|
-
if (endpoint === LEGACY_SECURENOW_GATEWAY) return
|
|
445
|
+
if (endpoint === LEGACY_SECURENOW_GATEWAY) return SECURENOW_CLOUD_INGEST;
|
|
444
446
|
|
|
445
447
|
try {
|
|
446
448
|
const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(endpoint) ? endpoint : `https://${endpoint}`;
|
|
447
449
|
const url = new URL(parseable);
|
|
448
450
|
if (url.port === '4318' && url.hostname.toLowerCase().endsWith('.securenow.ai')) {
|
|
449
|
-
return
|
|
451
|
+
return SECURENOW_CLOUD_INGEST;
|
|
450
452
|
}
|
|
451
453
|
} catch {}
|
|
452
454
|
|
|
@@ -626,7 +628,7 @@ function resolveApiKey() {
|
|
|
626
628
|
function resolveInstance() {
|
|
627
629
|
const fromConfig = pick(resolveConfigPath('otel.endpoint'));
|
|
628
630
|
if (fromConfig) return normalizeInstanceEndpoint(fromConfig);
|
|
629
|
-
return
|
|
631
|
+
return SECURENOW_CLOUD_INGEST;
|
|
630
632
|
}
|
|
631
633
|
|
|
632
634
|
function resolveAll() {
|
|
@@ -725,6 +727,7 @@ function resolveFirewallOptions() {
|
|
|
725
727
|
}
|
|
726
728
|
|
|
727
729
|
module.exports = {
|
|
730
|
+
SECURENOW_CLOUD_INGEST,
|
|
728
731
|
FREE_TRIAL_INSTANCE,
|
|
729
732
|
DEFAULT_API_URL,
|
|
730
733
|
DEFAULT_FIREWALL_API_URL,
|
package/cidr.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Bitmask-based CIDR matching. No regex on user input — prevents ReDoS.
|
|
5
|
-
* All operations use unsigned 32-bit integers for IPv4 addresses.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
function ipToInt(ip) {
|
|
9
|
-
const parts = ip.split('.');
|
|
10
|
-
if (parts.length !== 4) return null;
|
|
11
|
-
let result = 0;
|
|
12
|
-
for (let i = 0; i < 4; i++) {
|
|
13
|
-
const n = parseInt(parts[i], 10);
|
|
14
|
-
if (isNaN(n) || n < 0 || n > 255) return null;
|
|
15
|
-
result = (result << 8) + n;
|
|
16
|
-
}
|
|
17
|
-
return result >>> 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function parseCidr(cidr) {
|
|
21
|
-
const slash = cidr.indexOf('/');
|
|
22
|
-
if (slash === -1) return null;
|
|
23
|
-
const ip = cidr.slice(0, slash);
|
|
24
|
-
const bits = parseInt(cidr.slice(slash + 1), 10);
|
|
25
|
-
if (isNaN(bits) || bits < 0 || bits > 32) return null;
|
|
26
|
-
const network = ipToInt(ip);
|
|
27
|
-
if (network === null) return null;
|
|
28
|
-
const mask = bits === 0 ? 0 : (~((1 << (32 - bits)) - 1)) >>> 0;
|
|
29
|
-
return { network: (network & mask) >>> 0, mask };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function matchesCidr(ipInt, cidrEntry) {
|
|
33
|
-
return ((ipInt & cidrEntry.mask) >>> 0) === cidrEntry.network;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Create a matcher from a list of IPs and CIDRs.
|
|
38
|
-
* Returns { isBlocked(ip), stats() }.
|
|
39
|
-
* Exact IPs use a Set for O(1) lookup; CIDRs use array scan (typically < 100 entries).
|
|
40
|
-
*/
|
|
41
|
-
function createMatcher(ipList) {
|
|
42
|
-
const exactSet = new Set();
|
|
43
|
-
const cidrRanges = [];
|
|
44
|
-
|
|
45
|
-
for (const entry of ipList) {
|
|
46
|
-
const trimmed = (entry || '').trim();
|
|
47
|
-
if (!trimmed) continue;
|
|
48
|
-
|
|
49
|
-
if (trimmed.includes('/')) {
|
|
50
|
-
const parsed = parseCidr(trimmed);
|
|
51
|
-
if (parsed) cidrRanges.push(parsed);
|
|
52
|
-
} else {
|
|
53
|
-
const normalized = trimmed.replace(/^::ffff:/, '');
|
|
54
|
-
exactSet.add(normalized);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isBlocked(ip) {
|
|
59
|
-
if (!ip) return false;
|
|
60
|
-
const normalized = ip.replace(/^::ffff:/, '');
|
|
61
|
-
|
|
62
|
-
if (exactSet.has(normalized)) return true;
|
|
63
|
-
|
|
64
|
-
if (cidrRanges.length > 0) {
|
|
65
|
-
const ipInt = ipToInt(normalized);
|
|
66
|
-
if (ipInt !== null) {
|
|
67
|
-
for (const cidr of cidrRanges) {
|
|
68
|
-
if (matchesCidr(ipInt, cidr)) return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function stats() {
|
|
77
|
-
return { exact: exactSet.size, cidr: cidrRanges.length, total: exactSet.size + cidrRanges.length };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { isBlocked, stats };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = { ipToInt, parseCidr, matchesCidr, createMatcher };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bitmask-based CIDR matching. No regex on user input — prevents ReDoS.
|
|
5
|
+
* All operations use unsigned 32-bit integers for IPv4 addresses.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function ipToInt(ip) {
|
|
9
|
+
const parts = ip.split('.');
|
|
10
|
+
if (parts.length !== 4) return null;
|
|
11
|
+
let result = 0;
|
|
12
|
+
for (let i = 0; i < 4; i++) {
|
|
13
|
+
const n = parseInt(parts[i], 10);
|
|
14
|
+
if (isNaN(n) || n < 0 || n > 255) return null;
|
|
15
|
+
result = (result << 8) + n;
|
|
16
|
+
}
|
|
17
|
+
return result >>> 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseCidr(cidr) {
|
|
21
|
+
const slash = cidr.indexOf('/');
|
|
22
|
+
if (slash === -1) return null;
|
|
23
|
+
const ip = cidr.slice(0, slash);
|
|
24
|
+
const bits = parseInt(cidr.slice(slash + 1), 10);
|
|
25
|
+
if (isNaN(bits) || bits < 0 || bits > 32) return null;
|
|
26
|
+
const network = ipToInt(ip);
|
|
27
|
+
if (network === null) return null;
|
|
28
|
+
const mask = bits === 0 ? 0 : (~((1 << (32 - bits)) - 1)) >>> 0;
|
|
29
|
+
return { network: (network & mask) >>> 0, mask };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchesCidr(ipInt, cidrEntry) {
|
|
33
|
+
return ((ipInt & cidrEntry.mask) >>> 0) === cidrEntry.network;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a matcher from a list of IPs and CIDRs.
|
|
38
|
+
* Returns { isBlocked(ip), stats() }.
|
|
39
|
+
* Exact IPs use a Set for O(1) lookup; CIDRs use array scan (typically < 100 entries).
|
|
40
|
+
*/
|
|
41
|
+
function createMatcher(ipList) {
|
|
42
|
+
const exactSet = new Set();
|
|
43
|
+
const cidrRanges = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of ipList) {
|
|
46
|
+
const trimmed = (entry || '').trim();
|
|
47
|
+
if (!trimmed) continue;
|
|
48
|
+
|
|
49
|
+
if (trimmed.includes('/')) {
|
|
50
|
+
const parsed = parseCidr(trimmed);
|
|
51
|
+
if (parsed) cidrRanges.push(parsed);
|
|
52
|
+
} else {
|
|
53
|
+
const normalized = trimmed.replace(/^::ffff:/, '');
|
|
54
|
+
exactSet.add(normalized);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isBlocked(ip) {
|
|
59
|
+
if (!ip) return false;
|
|
60
|
+
const normalized = ip.replace(/^::ffff:/, '');
|
|
61
|
+
|
|
62
|
+
if (exactSet.has(normalized)) return true;
|
|
63
|
+
|
|
64
|
+
if (cidrRanges.length > 0) {
|
|
65
|
+
const ipInt = ipToInt(normalized);
|
|
66
|
+
if (ipInt !== null) {
|
|
67
|
+
for (const cidr of cidrRanges) {
|
|
68
|
+
if (matchesCidr(ipInt, cidr)) return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stats() {
|
|
77
|
+
return { exact: exactSet.size, cidr: cidrRanges.length, total: exactSet.size + cidrRanges.length };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { isBlocked, stats };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { ipToInt, parseCidr, matchesCidr, createMatcher };
|
package/cli/apps.js
CHANGED
|
@@ -4,11 +4,31 @@ const { api, requireAuth } = require('./client');
|
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
const ui = require('./ui');
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Shared SecureNow Cloud collector (the production ingestion gateway).
|
|
8
|
+
const SECURENOW_CLOUD_URL = 'https://ingest.securenow.ai';
|
|
9
|
+
const SECURENOW_CLOUD_NAME = 'SecureNow Cloud';
|
|
8
10
|
const DOMAIN_REGEX = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
9
11
|
|
|
10
12
|
function instanceUrl(inst) {
|
|
11
|
-
return
|
|
13
|
+
return SECURENOW_CLOUD_URL;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatUsd(amount) {
|
|
17
|
+
const n = Number(amount);
|
|
18
|
+
if (!Number.isFinite(n)) return null;
|
|
19
|
+
return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// POST /applications and /applications/bulk return `billing` for the account.
|
|
23
|
+
// Per-app subscribers pay for every app, so tell them the new monthly total.
|
|
24
|
+
// Human mode only: --json output stays exactly what it was.
|
|
25
|
+
function printBillingNotice(billing, flags) {
|
|
26
|
+
if (!billing || !billing.isPerAppSubscriber || (flags && flags.json)) return;
|
|
27
|
+
const count = Number(billing.appCount) || 0;
|
|
28
|
+
const price = formatUsd(billing.perAppPrice) || '$99';
|
|
29
|
+
const total = formatUsd(billing.estimatedMonthlyTotal);
|
|
30
|
+
const totalText = total ? ` → ${total}/month` : '';
|
|
31
|
+
ui.info(`Billing: this account now has ${count} app${count !== 1 ? 's' : ''}${totalText} (${price}/app, prorated).`);
|
|
12
32
|
}
|
|
13
33
|
|
|
14
34
|
async function fetchInstanceMap() {
|
|
@@ -51,7 +71,7 @@ async function list(args, flags) {
|
|
|
51
71
|
const rows = apps.map(app => {
|
|
52
72
|
const inst = app.instanceId ? instMap[app.instanceId] : null;
|
|
53
73
|
const url = instanceUrl(inst);
|
|
54
|
-
const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green(
|
|
74
|
+
const instLabel = inst ? `${inst.name} ${ui.c.dim(url)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(url)}`;
|
|
55
75
|
return [
|
|
56
76
|
app.name + (app.key === defaultApp ? ui.c.cyan(' (default)') : ''),
|
|
57
77
|
ui.c.dim(app.key),
|
|
@@ -120,7 +140,7 @@ async function create(args, flags) {
|
|
|
120
140
|
['Name', app.name],
|
|
121
141
|
['Key', ui.c.green(ui.c.bold(app.key))],
|
|
122
142
|
['ID', ui.c.dim(app._id)],
|
|
123
|
-
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(
|
|
143
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(`(${envUrl})`)}`],
|
|
124
144
|
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
125
145
|
]);
|
|
126
146
|
|
|
@@ -130,6 +150,11 @@ async function create(args, flags) {
|
|
|
130
150
|
console.log(` ${ui.c.dim('(writes .securenow/runtime.json - no env var needed)')}`);
|
|
131
151
|
console.log('');
|
|
132
152
|
|
|
153
|
+
if (!flags.json && result.billing?.isPerAppSubscriber) {
|
|
154
|
+
printBillingNotice(result.billing, flags);
|
|
155
|
+
console.log('');
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
if (flags.json) {
|
|
134
159
|
ui.json({ ...app, instanceUrl: envUrl });
|
|
135
160
|
}
|
|
@@ -155,9 +180,9 @@ async function pickInstance() {
|
|
|
155
180
|
return null;
|
|
156
181
|
}
|
|
157
182
|
|
|
158
|
-
const
|
|
183
|
+
const CLOUD_LABEL = `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim('— managed collector, no setup needed')}`;
|
|
159
184
|
|
|
160
|
-
const choices = [{ label:
|
|
185
|
+
const choices = [{ label: CLOUD_LABEL, value: null }];
|
|
161
186
|
|
|
162
187
|
for (const inst of instances) {
|
|
163
188
|
const status = inst.status === 'active' ? ui.c.green('●') : ui.c.red('●');
|
|
@@ -233,7 +258,7 @@ async function info(args, flags) {
|
|
|
233
258
|
ui.keyValue([
|
|
234
259
|
['Key', ui.c.bold(app.key)],
|
|
235
260
|
['ID', ui.c.dim(app._id)],
|
|
236
|
-
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(
|
|
261
|
+
['Instance', inst ? `${inst.name} ${ui.c.dim(`(${envUrl})`)}` : `${ui.c.green(SECURENOW_CLOUD_NAME)} ${ui.c.dim(`(${envUrl})`)}`],
|
|
237
262
|
['Hosts', app.hosts?.length ? app.hosts.join(', ') : ui.c.dim('none')],
|
|
238
263
|
['Created', app.createdAt ? new Date(app.createdAt).toLocaleString() : '—'],
|
|
239
264
|
['Updated', app.updatedAt ? new Date(app.updatedAt).toLocaleString() : '—'],
|
|
@@ -295,7 +320,7 @@ async function setDefault(args) {
|
|
|
295
320
|
name: app.name || null,
|
|
296
321
|
});
|
|
297
322
|
ui.success(`Default application set to ${ui.c.bold(app.name || key)}`);
|
|
298
|
-
ui.info(`SDK will use ${ui.c.dim(
|
|
323
|
+
ui.info(`SDK will use ${ui.c.dim(SECURENOW_CLOUD_URL)} for telemetry; the gateway routes by app key.`);
|
|
299
324
|
} catch {
|
|
300
325
|
// If we can't reach the API (offline / invalid key), still set the CLI default.
|
|
301
326
|
ui.success(`Default application set to ${ui.c.bold(key)}`);
|
|
@@ -418,6 +443,7 @@ async function discoverAndAdd(domains, instanceId, flags) {
|
|
|
418
443
|
]);
|
|
419
444
|
ui.table(['Name', 'Key', 'Hosts'], rows);
|
|
420
445
|
}
|
|
446
|
+
printBillingNotice(result.billing, flags);
|
|
421
447
|
console.log('');
|
|
422
448
|
} catch (err) {
|
|
423
449
|
createSpinner.fail('Failed to create applications');
|
|
@@ -535,6 +561,7 @@ async function scan(args, flags) {
|
|
|
535
561
|
});
|
|
536
562
|
totalCreated = result.count || subdomainNames.length;
|
|
537
563
|
createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
|
|
564
|
+
printBillingNotice(result.billing, flags);
|
|
538
565
|
} catch (err) {
|
|
539
566
|
createSpinner.fail('Failed to create applications');
|
|
540
567
|
throw err;
|
|
@@ -564,6 +591,7 @@ async function scan(args, flags) {
|
|
|
564
591
|
});
|
|
565
592
|
totalCreated = result.count || selected.length;
|
|
566
593
|
createSpinner.stop(`Created ${totalCreated} application${totalCreated !== 1 ? 's' : ''}`);
|
|
594
|
+
printBillingNotice(result.billing, flags);
|
|
567
595
|
} catch (err) {
|
|
568
596
|
createSpinner.fail('Failed to create applications');
|
|
569
597
|
throw err;
|
package/cli/client.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const { URL } = require('url');
|
|
6
|
-
const config = require('./config');
|
|
7
|
-
const ui = require('./ui');
|
|
8
|
-
|
|
9
|
-
function buildQueryString(params) {
|
|
10
|
-
if (!params) return '';
|
|
11
|
-
const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
|
|
12
|
-
if (!entries.length) return '';
|
|
13
|
-
return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
17
|
-
const baseUrl = config.getApiUrl();
|
|
18
|
-
const qs = buildQueryString(query);
|
|
19
|
-
const urlStr = `${baseUrl}/api${endpoint}${qs}`;
|
|
20
|
-
const url = new URL(urlStr);
|
|
21
|
-
const mod = url.protocol === 'https:' ? https : http;
|
|
22
|
-
|
|
23
|
-
const authToken = token || config.getToken();
|
|
24
|
-
|
|
25
|
-
return new Promise((resolve, reject) => {
|
|
26
|
-
const options = {
|
|
27
|
-
hostname: url.hostname,
|
|
28
|
-
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
29
|
-
path: url.pathname + url.search,
|
|
30
|
-
method,
|
|
31
|
-
headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (authToken) {
|
|
35
|
-
options.headers['Authorization'] = `Bearer ${authToken}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const req = mod.request(options, (res) => {
|
|
39
|
-
let data = '';
|
|
40
|
-
res.on('data', (chunk) => (data += chunk));
|
|
41
|
-
res.on('end', () => {
|
|
42
|
-
if (raw) {
|
|
43
|
-
return resolve({ status: res.statusCode, headers: res.headers, body: data });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let parsed;
|
|
47
|
-
try {
|
|
48
|
-
parsed = JSON.parse(data);
|
|
49
|
-
} catch {
|
|
50
|
-
parsed = data;
|
|
51
|
-
}
|
|
52
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { URL } = require('url');
|
|
6
|
+
const config = require('./config');
|
|
7
|
+
const ui = require('./ui');
|
|
8
|
+
|
|
9
|
+
function buildQueryString(params) {
|
|
10
|
+
if (!params) return '';
|
|
11
|
+
const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
|
|
12
|
+
if (!entries.length) return '';
|
|
13
|
+
return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
17
|
+
const baseUrl = config.getApiUrl();
|
|
18
|
+
const qs = buildQueryString(query);
|
|
19
|
+
const urlStr = `${baseUrl}/api${endpoint}${qs}`;
|
|
20
|
+
const url = new URL(urlStr);
|
|
21
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
22
|
+
|
|
23
|
+
const authToken = token || config.getToken();
|
|
24
|
+
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const options = {
|
|
27
|
+
hostname: url.hostname,
|
|
28
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
29
|
+
path: url.pathname + url.search,
|
|
30
|
+
method,
|
|
31
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (authToken) {
|
|
35
|
+
options.headers['Authorization'] = `Bearer ${authToken}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const req = mod.request(options, (res) => {
|
|
39
|
+
let data = '';
|
|
40
|
+
res.on('data', (chunk) => (data += chunk));
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
if (raw) {
|
|
43
|
+
return resolve({ status: res.statusCode, headers: res.headers, body: data });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(data);
|
|
49
|
+
} catch {
|
|
50
|
+
parsed = data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
53
|
if (res.statusCode === 401) {
|
|
54
54
|
reject(new CLIError('Admin auth is missing or expired. Run `securenow admin login` to re-authenticate. Runtime app credentials are unrelated.', 401));
|
|
55
55
|
return;
|
|
@@ -59,58 +59,58 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
|
|
|
59
59
|
reject(new CLIError(`${msg}. Admin auth is connected, but this user/plan may lack permission for this control-plane operation. Runtime app connection is unrelated.`, 403));
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
if (res.statusCode >= 400) {
|
|
63
|
-
const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
|
|
64
|
-
const details = parsed?.details || parsed?.unauthorizedKeys;
|
|
65
|
-
const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
|
|
66
|
-
reject(err);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
resolve(parsed);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
req.on('error', (err) => {
|
|
75
|
-
if (err.code === 'ECONNREFUSED') {
|
|
76
|
-
reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
|
|
77
|
-
} else if (err.code === 'ENOTFOUND') {
|
|
78
|
-
reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
|
|
79
|
-
} else {
|
|
80
|
-
reject(new CLIError(`Network error: ${err.message}`));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
if (body) {
|
|
85
|
-
req.write(JSON.stringify(body));
|
|
86
|
-
}
|
|
87
|
-
req.end();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
class CLIError extends Error {
|
|
92
|
-
constructor(message, statusCode) {
|
|
93
|
-
super(message);
|
|
94
|
-
this.name = 'CLIError';
|
|
95
|
-
this.statusCode = statusCode;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function requireAuth() {
|
|
62
|
+
if (res.statusCode >= 400) {
|
|
63
|
+
const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
|
|
64
|
+
const details = parsed?.details || parsed?.unauthorizedKeys;
|
|
65
|
+
const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
|
|
66
|
+
reject(err);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
resolve(parsed);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
req.on('error', (err) => {
|
|
75
|
+
if (err.code === 'ECONNREFUSED') {
|
|
76
|
+
reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
|
|
77
|
+
} else if (err.code === 'ENOTFOUND') {
|
|
78
|
+
reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
|
|
79
|
+
} else {
|
|
80
|
+
reject(new CLIError(`Network error: ${err.message}`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (body) {
|
|
85
|
+
req.write(JSON.stringify(body));
|
|
86
|
+
}
|
|
87
|
+
req.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class CLIError extends Error {
|
|
92
|
+
constructor(message, statusCode) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = 'CLIError';
|
|
95
|
+
this.statusCode = statusCode;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireAuth() {
|
|
100
100
|
const token = config.getToken();
|
|
101
101
|
if (!token) {
|
|
102
102
|
ui.error('Admin auth is not connected. Run `securenow admin login` first.');
|
|
103
103
|
process.exit(1);
|
|
104
104
|
}
|
|
105
|
-
return token;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const api = {
|
|
109
|
-
get: (endpoint, opts) => request('GET', endpoint, opts),
|
|
110
|
-
post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
|
|
111
|
-
put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
|
|
112
|
-
patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
|
|
113
|
-
delete: (endpoint, opts) => request('DELETE', endpoint, opts),
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
module.exports = { api, CLIError, requireAuth, buildQueryString };
|
|
105
|
+
return token;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const api = {
|
|
109
|
+
get: (endpoint, opts) => request('GET', endpoint, opts),
|
|
110
|
+
post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
|
|
111
|
+
put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
|
|
112
|
+
patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
|
|
113
|
+
delete: (endpoint, opts) => request('DELETE', endpoint, opts),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = { api, CLIError, requireAuth, buildQueryString };
|
package/cli/diagnostics.js
CHANGED
|
@@ -497,7 +497,7 @@ async function doctor(_args, flags) {
|
|
|
497
497
|
} else if (cfg.instance === 'https://api.securenow.ai/api/otlp') {
|
|
498
498
|
warnings.push('Using the legacy SecureNow telemetry gateway path. Regenerate credentials so telemetry uses https://ingest.securenow.ai.');
|
|
499
499
|
} else if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
|
|
500
|
-
warnings.push('Using the legacy
|
|
500
|
+
warnings.push('Using the legacy SecureNow Cloud collector address directly. Regenerate credentials so telemetry flows through https://ingest.securenow.ai.');
|
|
501
501
|
}
|
|
502
502
|
if (!cfg.apiKey && token) {
|
|
503
503
|
warnings.push('Admin CLI/MCP auth is connected, but the runtime API key is missing. Run `npx securenow app connect` or `npx securenow api-key create` to refresh .securenow/runtime.json.');
|
|
@@ -555,6 +555,10 @@ async function eventSend(args, flags) {
|
|
|
555
555
|
if (Object.keys(attributes).length) ev.attributes = attributes;
|
|
556
556
|
|
|
557
557
|
const headers = { 'Content-Type': 'application/json', ...cfg.headers };
|
|
558
|
+
// `--env` must win over the environment stored with the runtime credentials.
|
|
559
|
+
if ((flags.env || flags.environment) && cfg.deploymentEnvironment) {
|
|
560
|
+
headers['x-securenow-environment'] = cfg.deploymentEnvironment;
|
|
561
|
+
}
|
|
558
562
|
const spin = ui.spinner(`Sending event to ${eventsEndpoint}`);
|
|
559
563
|
try {
|
|
560
564
|
const res = await httpRequest({ endpoint: eventsEndpoint, headers, body: JSON.stringify({ events: [ev] }) });
|