securenow 7.5.1 → 7.6.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/CONSUMING-APPS-GUIDE.md +2 -0
- package/NPM_README.md +201 -237
- package/README.md +73 -26
- package/SKILL-API.md +209 -205
- package/SKILL-CLI.md +71 -64
- package/app-config.js +479 -83
- package/cli/apiKey.js +1 -1
- package/cli/apps.js +1 -1
- package/cli/config.js +31 -12
- package/cli/credentials.js +88 -0
- package/cli/diagnostics.js +68 -104
- package/cli/firewall.js +29 -14
- package/cli/init.js +208 -206
- package/cli/monitor.js +107 -43
- package/cli/security.js +24 -12
- package/cli/utils.js +2 -1
- package/cli.js +71 -39
- package/console-instrumentation.js +1 -1
- package/docs/ENVIRONMENT-VARIABLES.md +137 -863
- package/docs/ENVIRONMENTS.md +60 -0
- package/docs/EXPRESS-SETUP-GUIDE.md +3 -0
- package/docs/FIREWALL-GUIDE.md +3 -0
- package/docs/INDEX.md +6 -8
- package/docs/LOGGING-GUIDE.md +3 -0
- package/docs/MCP-GUIDE.md +8 -0
- package/docs/NEXTJS-GUIDE.md +3 -0
- package/docs/NEXTJS-QUICKSTART.md +24 -16
- package/docs/NUXT-GUIDE.md +3 -0
- package/docs/QUICKSTART-BODY-CAPTURE.md +3 -0
- package/docs/REQUEST-BODY-CAPTURE.md +3 -0
- package/firewall-cloud.js +10 -10
- package/firewall-only.js +25 -23
- package/firewall.js +47 -29
- package/free-trial-banner.js +1 -1
- package/mcp/catalog.js +104 -17
- package/nextjs-auto-capture.d.ts +7 -4
- package/nextjs-auto-capture.js +7 -7
- package/nextjs-middleware.js +4 -3
- package/nextjs-wrapper.js +6 -6
- package/nextjs.d.ts +36 -25
- package/nextjs.js +47 -55
- package/nuxt-server-plugin.mjs +35 -51
- package/nuxt.d.ts +29 -23
- package/package.json +1 -1
- package/postinstall.js +27 -61
- package/register.d.ts +19 -33
- package/register.js +8 -8
- package/resolve-ip.js +4 -5
- package/tracing.d.ts +21 -19
- package/tracing.js +34 -42
package/app-config.js
CHANGED
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Shared
|
|
4
|
+
* Shared SecureNow configuration resolver.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* AND as x-api-key header. The collector
|
|
11
|
-
* filters ClickHouse queries by
|
|
12
|
-
* `resource_string_service$$name IN (...)`
|
|
13
|
-
* so service.name MUST be the app.key UUID.
|
|
14
|
-
* - What human label to show? (resolveAppName) — display only, never
|
|
15
|
-
* sent to the collector.
|
|
16
|
-
* - Which OTLP collector to hit? (resolveInstance)
|
|
17
|
-
* - Which firewall API key? (resolveApiKey) — the snk_live_... key the
|
|
18
|
-
* firewall sends as Bearer to /api/firewall
|
|
19
|
-
* for blocklist sync. Separate from the
|
|
20
|
-
* app routing key: this one is user-scoped.
|
|
21
|
-
*
|
|
22
|
-
* Resolution order (first non-empty wins):
|
|
23
|
-
*
|
|
24
|
-
* 1. Explicit environment variable
|
|
25
|
-
* (SECURENOW_APPID / SECURENOW_API_KEY / SECURENOW_INSTANCE)
|
|
26
|
-
* 2. Project-local credentials (./.securenow/credentials.json)
|
|
27
|
-
* 3. Global credentials (~/.securenow/credentials.json)
|
|
28
|
-
* 4. package.json#name (for the human label only — can't route on this)
|
|
29
|
-
* 5. Hard default / null
|
|
30
|
-
*
|
|
31
|
-
* Credentials file schema:
|
|
32
|
-
* { token, email, expiresAt, apiKey: <snk_live_...>, app: { key, name, instance } }
|
|
6
|
+
* Local development and production are driven by ./.securenow/credentials.json.
|
|
7
|
+
* Legacy environment variables are only fallback inputs for existing installs;
|
|
8
|
+
* every SDK setting has a file-backed equivalent so customers do not need .env
|
|
9
|
+
* files.
|
|
33
10
|
*/
|
|
34
11
|
|
|
35
12
|
const fs = require('fs');
|
|
@@ -37,6 +14,147 @@ const path = require('path');
|
|
|
37
14
|
const os = require('os');
|
|
38
15
|
|
|
39
16
|
const FREE_TRIAL_INSTANCE = 'https://freetrial.securenow.ai:4318';
|
|
17
|
+
const DEFAULT_API_URL = 'https://api.securenow.ai';
|
|
18
|
+
const CONFIG_SCHEMA_VERSION = 2;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG = Object.freeze({
|
|
21
|
+
logging: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
},
|
|
24
|
+
capture: {
|
|
25
|
+
body: true,
|
|
26
|
+
multipart: true,
|
|
27
|
+
maxBodySize: 10240,
|
|
28
|
+
sensitiveFields: [],
|
|
29
|
+
},
|
|
30
|
+
otel: {
|
|
31
|
+
endpoint: null,
|
|
32
|
+
tracesEndpoint: null,
|
|
33
|
+
logsEndpoint: null,
|
|
34
|
+
headers: {},
|
|
35
|
+
logLevel: 'none',
|
|
36
|
+
disableInstrumentations: [],
|
|
37
|
+
},
|
|
38
|
+
runtime: {
|
|
39
|
+
deploymentEnvironment: 'production',
|
|
40
|
+
noUuid: null,
|
|
41
|
+
strict: false,
|
|
42
|
+
testSpan: false,
|
|
43
|
+
hideBanner: false,
|
|
44
|
+
},
|
|
45
|
+
firewall: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
apiUrl: DEFAULT_API_URL,
|
|
48
|
+
versionCheckInterval: 10,
|
|
49
|
+
syncInterval: 300,
|
|
50
|
+
failMode: 'open',
|
|
51
|
+
statusCode: 403,
|
|
52
|
+
log: true,
|
|
53
|
+
tcp: false,
|
|
54
|
+
iptables: false,
|
|
55
|
+
cloud: null,
|
|
56
|
+
cloudDryRun: false,
|
|
57
|
+
cloudflare: {
|
|
58
|
+
apiToken: null,
|
|
59
|
+
accountId: null,
|
|
60
|
+
},
|
|
61
|
+
aws: {
|
|
62
|
+
wafIpSetId: null,
|
|
63
|
+
wafIpSetName: 'securenow-blocklist',
|
|
64
|
+
wafScope: 'REGIONAL',
|
|
65
|
+
},
|
|
66
|
+
gcp: {
|
|
67
|
+
projectId: null,
|
|
68
|
+
securityPolicy: null,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
networking: {
|
|
72
|
+
trustedProxies: [],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const CONFIG_EXPLANATIONS = Object.freeze({
|
|
77
|
+
'token': 'CLI session token written by `npx securenow login`. Secret: do not commit.',
|
|
78
|
+
'apiKey': 'Scoped firewall API key (`snk_live_...`) minted by login or `securenow api-key set`. Secret: do not commit.',
|
|
79
|
+
'app.key': 'SecureNow application routing UUID. The SDK uses this as OTel service.name so dashboard queries match exactly.',
|
|
80
|
+
'app.name': 'Human-readable app label shown in CLI output.',
|
|
81
|
+
'app.instance': 'OTLP collector base URL for this app. Production should provide this same credentials file at .securenow/credentials.json.',
|
|
82
|
+
'config.logging.enabled': 'Secure default: console log forwarding is on. Set false to disable OTLP logs.',
|
|
83
|
+
'config.capture.body': 'Secure default: JSON, GraphQL, and form request bodies are captured with redaction.',
|
|
84
|
+
'config.capture.multipart': 'Secure default: multipart text fields and file metadata are captured. File content is never buffered.',
|
|
85
|
+
'config.capture.maxBodySize': 'Maximum body bytes captured per request. Default 10KB limits memory and sensitive data exposure.',
|
|
86
|
+
'config.capture.sensitiveFields': 'Extra field-name fragments to redact in addition to SecureNow built-ins.',
|
|
87
|
+
'config.otel.endpoint': 'Optional OTLP base endpoint override. Usually app.instance is enough.',
|
|
88
|
+
'config.otel.tracesEndpoint': 'Optional full traces endpoint override, for split collectors.',
|
|
89
|
+
'config.otel.logsEndpoint': 'Optional full logs endpoint override, for split collectors.',
|
|
90
|
+
'config.otel.headers': 'Optional OTLP headers. The SDK auto-adds x-api-key from app.key when missing.',
|
|
91
|
+
'config.otel.logLevel': 'OpenTelemetry diagnostic log level: none, error, warn, info, or debug.',
|
|
92
|
+
'config.otel.disableInstrumentations': 'Optional OTel instrumentation package names to disable.',
|
|
93
|
+
'config.runtime.deploymentEnvironment': 'deployment.environment resource attribute. Set this in the credentials file for production.',
|
|
94
|
+
'config.runtime.noUuid': 'null means auto: true when app.key is present. Set true/false only for advanced routing needs.',
|
|
95
|
+
'config.runtime.strict': 'If true, PM2/cluster workers exit when no app identity is resolvable.',
|
|
96
|
+
'config.runtime.testSpan': 'If true, emit a startup smoke span. Prefer `npx securenow test-span` for manual checks.',
|
|
97
|
+
'config.runtime.hideBanner': 'Hide the free-trial response banner when using the managed free-trial collector.',
|
|
98
|
+
'config.firewall.enabled': 'Secure default: app firewall enforcement starts when apiKey is present and the dashboard toggle is on.',
|
|
99
|
+
'config.firewall.apiUrl': 'SecureNow API base URL for firewall sync.',
|
|
100
|
+
'config.firewall.versionCheckInterval': 'Seconds between lightweight firewall version checks.',
|
|
101
|
+
'config.firewall.syncInterval': 'Seconds between full firewall blocklist syncs.',
|
|
102
|
+
'config.firewall.failMode': 'open allows traffic if SecureNow is temporarily unreachable; closed blocks all on sync failure.',
|
|
103
|
+
'config.firewall.statusCode': 'HTTP status returned by application-layer firewall blocks.',
|
|
104
|
+
'config.firewall.log': 'Log firewall decisions locally.',
|
|
105
|
+
'config.firewall.tcp': 'Layer 2 TCP drop. Default false because it patches sockets.',
|
|
106
|
+
'config.firewall.iptables': 'Layer 3 OS firewall. Default false because it requires Linux root/CAP_NET_ADMIN.',
|
|
107
|
+
'config.firewall.cloud': 'Layer 4 WAF provider: cloudflare, aws, gcp, or null.',
|
|
108
|
+
'config.firewall.cloudDryRun': 'Log intended cloud WAF pushes without applying them.',
|
|
109
|
+
'config.firewall.cloudflare': 'Cloudflare API token/account id for Layer 4 WAF. Secrets: do not commit.',
|
|
110
|
+
'config.firewall.aws': 'AWS WAF IP set configuration for Layer 4 WAF.',
|
|
111
|
+
'config.firewall.gcp': 'GCP Cloud Armor policy configuration for Layer 4 WAF.',
|
|
112
|
+
'config.networking.trustedProxies': 'Additional proxy IPs whose X-Forwarded-For headers should be trusted.',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const ENV_TO_CONFIG_PATH = Object.freeze({
|
|
116
|
+
SECURENOW_LOGGING_ENABLED: 'logging.enabled',
|
|
117
|
+
SECURENOW_CAPTURE_BODY: 'capture.body',
|
|
118
|
+
SECURENOW_CAPTURE_MULTIPART: 'capture.multipart',
|
|
119
|
+
SECURENOW_MAX_BODY_SIZE: 'capture.maxBodySize',
|
|
120
|
+
SECURENOW_SENSITIVE_FIELDS: 'capture.sensitiveFields',
|
|
121
|
+
SECURENOW_DISABLE_INSTRUMENTATIONS: 'otel.disableInstrumentations',
|
|
122
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'otel.endpoint',
|
|
123
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: 'otel.tracesEndpoint',
|
|
124
|
+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: 'otel.logsEndpoint',
|
|
125
|
+
OTEL_EXPORTER_OTLP_HEADERS: 'otel.headers',
|
|
126
|
+
OTEL_LOG_LEVEL: 'otel.logLevel',
|
|
127
|
+
SECURENOW_ENVIRONMENT: 'runtime.deploymentEnvironment',
|
|
128
|
+
SECURENOW_DEPLOYMENT_ENVIRONMENT: 'runtime.deploymentEnvironment',
|
|
129
|
+
NODE_ENV: 'runtime.deploymentEnvironment',
|
|
130
|
+
SECURENOW_NO_UUID: 'runtime.noUuid',
|
|
131
|
+
SECURENOW_STRICT: 'runtime.strict',
|
|
132
|
+
SECURENOW_TEST_SPAN: 'runtime.testSpan',
|
|
133
|
+
SECURENOW_HIDE_BANNER: 'runtime.hideBanner',
|
|
134
|
+
SECURENOW_FIREWALL_ENABLED: 'firewall.enabled',
|
|
135
|
+
SECURENOW_API_URL: 'firewall.apiUrl',
|
|
136
|
+
SECURENOW_FIREWALL_VERSION_INTERVAL: 'firewall.versionCheckInterval',
|
|
137
|
+
SECURENOW_FIREWALL_SYNC_INTERVAL: 'firewall.syncInterval',
|
|
138
|
+
SECURENOW_FIREWALL_FAIL_MODE: 'firewall.failMode',
|
|
139
|
+
SECURENOW_FIREWALL_STATUS_CODE: 'firewall.statusCode',
|
|
140
|
+
SECURENOW_FIREWALL_LOG: 'firewall.log',
|
|
141
|
+
SECURENOW_FIREWALL_TCP: 'firewall.tcp',
|
|
142
|
+
SECURENOW_FIREWALL_IPTABLES: 'firewall.iptables',
|
|
143
|
+
SECURENOW_FIREWALL_CLOUD: 'firewall.cloud',
|
|
144
|
+
SECURENOW_FIREWALL_CLOUD_DRY_RUN: 'firewall.cloudDryRun',
|
|
145
|
+
CLOUDFLARE_API_TOKEN: 'firewall.cloudflare.apiToken',
|
|
146
|
+
CLOUDFLARE_ACCOUNT_ID: 'firewall.cloudflare.accountId',
|
|
147
|
+
AWS_WAF_IP_SET_ID: 'firewall.aws.wafIpSetId',
|
|
148
|
+
AWS_WAF_IP_SET_NAME: 'firewall.aws.wafIpSetName',
|
|
149
|
+
AWS_WAF_SCOPE: 'firewall.aws.wafScope',
|
|
150
|
+
GCP_PROJECT_ID: 'firewall.gcp.projectId',
|
|
151
|
+
GCP_SECURITY_POLICY: 'firewall.gcp.securityPolicy',
|
|
152
|
+
SECURENOW_TRUSTED_PROXIES: 'networking.trustedProxies',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function clone(value) {
|
|
156
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
157
|
+
}
|
|
40
158
|
|
|
41
159
|
function readJsonSafe(filepath) {
|
|
42
160
|
try {
|
|
@@ -49,7 +167,7 @@ function readJsonSafe(filepath) {
|
|
|
49
167
|
function loadLocalCredentials() {
|
|
50
168
|
try {
|
|
51
169
|
const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '.';
|
|
52
|
-
return readJsonSafe(path.join(cwd, '.securenow', 'credentials.json'));
|
|
170
|
+
return withCredentialDefaults(readJsonSafe(path.join(cwd, '.securenow', 'credentials.json')));
|
|
53
171
|
} catch {
|
|
54
172
|
return null;
|
|
55
173
|
}
|
|
@@ -57,14 +175,21 @@ function loadLocalCredentials() {
|
|
|
57
175
|
|
|
58
176
|
function loadGlobalCredentials() {
|
|
59
177
|
try {
|
|
60
|
-
return readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json'));
|
|
178
|
+
return withCredentialDefaults(readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json')));
|
|
61
179
|
} catch {
|
|
62
180
|
return null;
|
|
63
181
|
}
|
|
64
182
|
}
|
|
65
183
|
|
|
66
184
|
function loadCredentials() {
|
|
67
|
-
|
|
185
|
+
try {
|
|
186
|
+
const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : '.';
|
|
187
|
+
const local = readJsonSafe(path.join(cwd, '.securenow', 'credentials.json'));
|
|
188
|
+
const global = readJsonSafe(path.join(os.homedir(), '.securenow', 'credentials.json'));
|
|
189
|
+
return withCredentialDefaults(mergeCredentials(global, local));
|
|
190
|
+
} catch {
|
|
191
|
+
return loadLocalCredentials() || loadGlobalCredentials() || null;
|
|
192
|
+
}
|
|
68
193
|
}
|
|
69
194
|
|
|
70
195
|
function loadPackageJsonName() {
|
|
@@ -78,70 +203,236 @@ function loadPackageJsonName() {
|
|
|
78
203
|
return null;
|
|
79
204
|
}
|
|
80
205
|
|
|
206
|
+
function mergeMissing(target, defaults) {
|
|
207
|
+
const out = target && typeof target === 'object' && !Array.isArray(target) ? clone(target) : {};
|
|
208
|
+
for (const [key, value] of Object.entries(defaults || {})) {
|
|
209
|
+
if (out[key] === undefined) {
|
|
210
|
+
out[key] = clone(value);
|
|
211
|
+
} else if (
|
|
212
|
+
value &&
|
|
213
|
+
typeof value === 'object' &&
|
|
214
|
+
!Array.isArray(value) &&
|
|
215
|
+
out[key] &&
|
|
216
|
+
typeof out[key] === 'object' &&
|
|
217
|
+
!Array.isArray(out[key])
|
|
218
|
+
) {
|
|
219
|
+
out[key] = mergeMissing(out[key], value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function deepOverlay(base, overlay) {
|
|
226
|
+
if (!base || typeof base !== 'object' || Array.isArray(base)) base = {};
|
|
227
|
+
if (!overlay || typeof overlay !== 'object' || Array.isArray(overlay)) return clone(base);
|
|
228
|
+
|
|
229
|
+
const out = clone(base);
|
|
230
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
231
|
+
if (value === undefined) continue;
|
|
232
|
+
if (
|
|
233
|
+
value &&
|
|
234
|
+
typeof value === 'object' &&
|
|
235
|
+
!Array.isArray(value) &&
|
|
236
|
+
out[key] &&
|
|
237
|
+
typeof out[key] === 'object' &&
|
|
238
|
+
!Array.isArray(out[key])
|
|
239
|
+
) {
|
|
240
|
+
out[key] = deepOverlay(out[key], value);
|
|
241
|
+
} else {
|
|
242
|
+
out[key] = clone(value);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function mergeCredentials(globalCredentials, localCredentials) {
|
|
249
|
+
if (!globalCredentials && !localCredentials) return null;
|
|
250
|
+
return deepOverlay(globalCredentials || {}, localCredentials || {});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function withCredentialDefaults(credentials) {
|
|
254
|
+
if (!credentials || typeof credentials !== 'object') return null;
|
|
255
|
+
const out = clone(credentials);
|
|
256
|
+
out.config = mergeMissing(out.config, DEFAULT_CONFIG);
|
|
257
|
+
out._securenow = mergeMissing(out._securenow, {
|
|
258
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
259
|
+
note: 'Local SecureNow credentials and secure SDK defaults. This file may contain secrets; keep .securenow/ in .gitignore.',
|
|
260
|
+
precedence: 'The same credentials file works in local development and production. Legacy environment variables are only fallback inputs when this file does not define a value.',
|
|
261
|
+
explanations: CONFIG_EXPLANATIONS,
|
|
262
|
+
});
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getPath(obj, dotted) {
|
|
267
|
+
if (!obj || !dotted) return undefined;
|
|
268
|
+
let cur = obj;
|
|
269
|
+
for (const part of dotted.split('.')) {
|
|
270
|
+
if (!cur || typeof cur !== 'object' || !(part in cur)) return undefined;
|
|
271
|
+
cur = cur[part];
|
|
272
|
+
}
|
|
273
|
+
return cur;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function rawEnv(key) {
|
|
277
|
+
return process.env[key] ?? process.env[key.toUpperCase()] ?? process.env[key.toLowerCase()];
|
|
278
|
+
}
|
|
279
|
+
|
|
81
280
|
function pick(value) {
|
|
82
281
|
if (value === undefined || value === null) return null;
|
|
83
|
-
|
|
84
|
-
|
|
282
|
+
if (typeof value === 'string') {
|
|
283
|
+
const str = value.trim();
|
|
284
|
+
return str.length > 0 ? str : null;
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
85
287
|
}
|
|
86
288
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
289
|
+
function parseBool(value, fallback = false) {
|
|
290
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
291
|
+
if (typeof value === 'boolean') return value;
|
|
292
|
+
if (typeof value === 'number') return value !== 0;
|
|
293
|
+
const text = String(value).trim().toLowerCase();
|
|
294
|
+
if (['0', 'false', 'no', 'off', 'disabled'].includes(text)) return false;
|
|
295
|
+
if (['1', 'true', 'yes', 'on', 'enabled'].includes(text)) return true;
|
|
296
|
+
return fallback;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parseNumber(value, fallback, min) {
|
|
300
|
+
const n = Number.parseInt(value, 10);
|
|
301
|
+
const resolved = Number.isFinite(n) ? n : fallback;
|
|
302
|
+
return typeof min === 'number' ? Math.max(min, resolved) : resolved;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseList(value) {
|
|
306
|
+
if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
307
|
+
if (value == null || value === '') return [];
|
|
308
|
+
return String(value).split(',').map((s) => s.trim()).filter(Boolean);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function normalizeDeploymentEnvironment(value) {
|
|
312
|
+
const raw = pick(value);
|
|
313
|
+
if (raw == null) return 'production';
|
|
314
|
+
const text = String(raw).trim().toLowerCase();
|
|
315
|
+
if (text === 'dev' || text === 'development') return 'local';
|
|
316
|
+
if (text === 'prod') return 'production';
|
|
317
|
+
if (!/^[a-z0-9_.-]{1,64}$/.test(text)) return 'production';
|
|
318
|
+
return text;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseHeaders(value) {
|
|
322
|
+
const out = {};
|
|
323
|
+
if (!value) return out;
|
|
324
|
+
|
|
325
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
326
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
327
|
+
const k = String(key).trim().toLowerCase();
|
|
328
|
+
const v = pick(rawValue);
|
|
329
|
+
if (k && v != null) out[k] = String(v);
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const raw of String(value).split(',')) {
|
|
335
|
+
const s = raw.trim();
|
|
336
|
+
if (!s) continue;
|
|
337
|
+
const i = s.indexOf('=');
|
|
338
|
+
if (i === -1) continue;
|
|
339
|
+
const key = s.slice(0, i).trim().toLowerCase();
|
|
340
|
+
const val = s.slice(i + 1).trim();
|
|
341
|
+
if (key && val) out[key] = val;
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function headersToString(headers) {
|
|
347
|
+
if (!headers || typeof headers !== 'object') return '';
|
|
348
|
+
return Object.entries(headers)
|
|
349
|
+
.filter(([, value]) => pick(value) != null)
|
|
350
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
351
|
+
.join(',');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function toEnvString(value) {
|
|
355
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
356
|
+
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
357
|
+
if (Array.isArray(value)) return value.join(',');
|
|
358
|
+
if (typeof value === 'object') return headersToString(value);
|
|
359
|
+
return String(value);
|
|
360
|
+
}
|
|
95
361
|
|
|
362
|
+
function resolveConfigPath(configPath, envKeys = [], fallback) {
|
|
96
363
|
const creds = loadCredentials();
|
|
97
|
-
|
|
364
|
+
const fromCreds = pick(getPath(creds && creds.config, configPath));
|
|
365
|
+
if (fromCreds != null) return fromCreds;
|
|
366
|
+
|
|
367
|
+
for (const key of envKeys) {
|
|
368
|
+
const fromEnv = pick(rawEnv(key));
|
|
369
|
+
if (fromEnv != null) return fromEnv;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const fromDefault = pick(getPath(DEFAULT_CONFIG, configPath));
|
|
373
|
+
if (fromDefault != null) return fromDefault;
|
|
374
|
+
|
|
375
|
+
return fallback;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function resolveAppKey() {
|
|
379
|
+
const creds = loadCredentials();
|
|
380
|
+
if (creds && creds.app && pick(creds.app.key)) return String(pick(creds.app.key));
|
|
381
|
+
|
|
382
|
+
const fromEnv = pick(rawEnv('SECURENOW_APPID')) || pick(rawEnv('securenow'));
|
|
383
|
+
if (fromEnv) return String(fromEnv);
|
|
384
|
+
|
|
385
|
+
// Legacy compatibility: older docs sometimes used SECURENOW_API_KEY as the
|
|
386
|
+
// app routing UUID. Real firewall keys start with snk_live_ and are handled
|
|
387
|
+
// by resolveApiKey(), not as service.name.
|
|
388
|
+
const legacyApiKey = pick(rawEnv('SECURENOW_API_KEY'));
|
|
389
|
+
if (legacyApiKey && !String(legacyApiKey).startsWith('snk_live_')) {
|
|
390
|
+
return String(legacyApiKey);
|
|
391
|
+
}
|
|
392
|
+
|
|
98
393
|
return null;
|
|
99
394
|
}
|
|
100
395
|
|
|
101
|
-
// Human label for observability tools — never touches collector routing.
|
|
102
|
-
// Falls back to package.json#name so pre-login users still see something readable.
|
|
103
396
|
function resolveAppName() {
|
|
104
|
-
const fromEnv = pick(process.env.OTEL_SERVICE_NAME);
|
|
105
|
-
if (fromEnv) return fromEnv;
|
|
106
|
-
|
|
107
397
|
const creds = loadCredentials();
|
|
108
|
-
if (creds && creds.app && pick(creds.app.name)) return pick(creds.app.name);
|
|
398
|
+
if (creds && creds.app && pick(creds.app.name)) return String(pick(creds.app.name));
|
|
399
|
+
|
|
400
|
+
const fromEnv = pick(rawEnv('OTEL_SERVICE_NAME'));
|
|
401
|
+
if (fromEnv) return String(fromEnv);
|
|
109
402
|
|
|
110
403
|
return loadPackageJsonName();
|
|
111
404
|
}
|
|
112
405
|
|
|
113
|
-
// Legacy alias — callers that want "whatever identifies this process to OTel"
|
|
114
|
-
// get the routing key if logged in, otherwise the human label fallback.
|
|
115
406
|
function resolveAppId() {
|
|
116
407
|
return resolveAppKey() || resolveAppName();
|
|
117
408
|
}
|
|
118
409
|
|
|
119
|
-
// Firewall / user-scoped API key (snk_live_...).
|
|
120
|
-
// Distinct from resolveAppKey: that one is the application UUID for OTel
|
|
121
|
-
// routing. This one authenticates the firewall blocklist sync to /api/firewall,
|
|
122
|
-
// so it must look like a real `snk_live_` key — the app UUID won't pass auth.
|
|
123
410
|
function resolveApiKey() {
|
|
124
|
-
const fromEnv = pick(process.env.SECURENOW_API_KEY);
|
|
125
|
-
if (fromEnv && fromEnv.startsWith('snk_live_')) return fromEnv;
|
|
126
|
-
|
|
127
411
|
const creds = loadCredentials();
|
|
128
412
|
const fromCreds = creds && pick(creds.apiKey);
|
|
129
|
-
if (fromCreds && fromCreds.startsWith('snk_live_')) return fromCreds;
|
|
413
|
+
if (fromCreds && String(fromCreds).startsWith('snk_live_')) return String(fromCreds);
|
|
414
|
+
|
|
415
|
+
const fromEnv = pick(rawEnv('SECURENOW_API_KEY'));
|
|
416
|
+
if (fromEnv && String(fromEnv).startsWith('snk_live_')) return String(fromEnv);
|
|
130
417
|
|
|
131
418
|
return null;
|
|
132
419
|
}
|
|
133
420
|
|
|
134
421
|
function resolveInstance() {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
pick(process.env.securenow_instance) ||
|
|
138
|
-
pick(process.env.OTEL_EXPORTER_OTLP_ENDPOINT);
|
|
139
|
-
if (fromEnv) return fromEnv.replace(/\/$/, '');
|
|
422
|
+
const fromConfig = pick(resolveConfigPath('otel.endpoint'));
|
|
423
|
+
if (fromConfig) return String(fromConfig).replace(/\/$/, '');
|
|
140
424
|
|
|
141
425
|
const creds = loadCredentials();
|
|
142
426
|
if (creds && creds.app && pick(creds.app.instance)) {
|
|
143
|
-
return pick(creds.app.instance).replace(/\/$/, '');
|
|
427
|
+
return String(pick(creds.app.instance)).replace(/\/$/, '');
|
|
144
428
|
}
|
|
429
|
+
|
|
430
|
+
const fromEnv =
|
|
431
|
+
pick(rawEnv('SECURENOW_INSTANCE')) ||
|
|
432
|
+
pick(rawEnv('securenow_instance')) ||
|
|
433
|
+
pick(rawEnv('OTEL_EXPORTER_OTLP_ENDPOINT'));
|
|
434
|
+
if (fromEnv) return String(fromEnv).replace(/\/$/, '');
|
|
435
|
+
|
|
145
436
|
return FREE_TRIAL_INSTANCE;
|
|
146
437
|
}
|
|
147
438
|
|
|
@@ -150,46 +441,151 @@ function resolveAll() {
|
|
|
150
441
|
return {
|
|
151
442
|
appKey,
|
|
152
443
|
appName: resolveAppName(),
|
|
153
|
-
// service.name must be the routing UUID when available — that's what the
|
|
154
|
-
// collector filters on. Fall back to the human label pre-login.
|
|
155
444
|
appId: appKey || resolveAppName(),
|
|
156
445
|
instance: resolveInstance(),
|
|
446
|
+
deploymentEnvironment: resolveDeploymentEnvironment(),
|
|
157
447
|
};
|
|
158
448
|
}
|
|
159
449
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// 2. Explicit SECURENOW_NO_UUID env var (0/1/true/false) — wins
|
|
171
|
-
// 3. appKey resolved (logged-in, appId is routing UUID) → true
|
|
172
|
-
// 4. Otherwise (pre-login, appId = package.json#name) → false
|
|
450
|
+
function resolveDeploymentEnvironment() {
|
|
451
|
+
return normalizeDeploymentEnvironment(
|
|
452
|
+
resolveConfigPath('runtime.deploymentEnvironment', [
|
|
453
|
+
'SECURENOW_ENVIRONMENT',
|
|
454
|
+
'SECURENOW_DEPLOYMENT_ENVIRONMENT',
|
|
455
|
+
'NODE_ENV',
|
|
456
|
+
])
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
173
460
|
function resolveNoUuid(opts = {}) {
|
|
174
461
|
if (opts.noUuid !== undefined && opts.noUuid !== null) return !!opts.noUuid;
|
|
175
462
|
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
463
|
+
const fromConfig = pick(getPath(loadCredentials()?.config, 'runtime.noUuid'));
|
|
464
|
+
if (fromConfig !== null) return parseBool(fromConfig, false);
|
|
465
|
+
|
|
466
|
+
const raw = pick(rawEnv('SECURENOW_NO_UUID'));
|
|
467
|
+
if (raw !== null) return parseBool(raw, false);
|
|
180
468
|
|
|
181
469
|
return !!resolveAppKey();
|
|
182
470
|
}
|
|
183
471
|
|
|
472
|
+
function resolveEnvKey(key) {
|
|
473
|
+
const upper = String(key).toUpperCase();
|
|
474
|
+
|
|
475
|
+
if (upper === 'SECURENOW_APPID') return resolveAppKey();
|
|
476
|
+
if (upper === 'OTEL_SERVICE_NAME') return resolveAppName();
|
|
477
|
+
if (upper === 'SECURENOW_API_KEY') return resolveApiKey();
|
|
478
|
+
if (upper === 'SECURENOW_INSTANCE' || upper === 'OTEL_EXPORTER_OTLP_ENDPOINT') return resolveInstance();
|
|
479
|
+
|
|
480
|
+
const configPath = ENV_TO_CONFIG_PATH[upper];
|
|
481
|
+
if (configPath) return resolveConfigPath(configPath);
|
|
482
|
+
|
|
483
|
+
const fromEnv = pick(rawEnv(upper));
|
|
484
|
+
if (fromEnv != null) return fromEnv;
|
|
485
|
+
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function env(key) {
|
|
490
|
+
return toEnvString(resolveEnvKey(key));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function boolEnv(key, fallback = false) {
|
|
494
|
+
return parseBool(resolveEnvKey(key), fallback);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function numberEnv(key, fallback, min) {
|
|
498
|
+
return parseNumber(resolveEnvKey(key), fallback, min);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function listEnv(key) {
|
|
502
|
+
return parseList(resolveEnvKey(key));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function resolveOtlpHeaders() {
|
|
506
|
+
const headers = parseHeaders(resolveConfigPath('otel.headers', ['OTEL_EXPORTER_OTLP_HEADERS'], {}));
|
|
507
|
+
const appKey = resolveAppKey();
|
|
508
|
+
if (appKey && !headers['x-api-key']) {
|
|
509
|
+
headers['x-api-key'] = appKey;
|
|
510
|
+
}
|
|
511
|
+
return headers;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function resolveOtlpHeaderString() {
|
|
515
|
+
return headersToString(resolveOtlpHeaders());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function resolveEndpoints(options = {}) {
|
|
519
|
+
const endpointBase = String(options.endpoint || resolveInstance()).replace(/\/$/, '');
|
|
520
|
+
return {
|
|
521
|
+
endpointBase,
|
|
522
|
+
tracesUrl: env('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT') || `${endpointBase}/v1/traces`,
|
|
523
|
+
logsUrl: env('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') || `${endpointBase}/v1/logs`,
|
|
524
|
+
headers: resolveOtlpHeaders(),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function resolveFirewallOptions() {
|
|
529
|
+
return {
|
|
530
|
+
apiKey: resolveApiKey(),
|
|
531
|
+
appKey: resolveAppKey() || null,
|
|
532
|
+
environment: resolveDeploymentEnvironment(),
|
|
533
|
+
enabled: boolEnv('SECURENOW_FIREWALL_ENABLED', true),
|
|
534
|
+
apiUrl: env('SECURENOW_API_URL') || DEFAULT_API_URL,
|
|
535
|
+
versionCheckInterval: numberEnv('SECURENOW_FIREWALL_VERSION_INTERVAL', 10, 1),
|
|
536
|
+
syncInterval: numberEnv('SECURENOW_FIREWALL_SYNC_INTERVAL', 300, 1),
|
|
537
|
+
failMode: env('SECURENOW_FIREWALL_FAIL_MODE') || 'open',
|
|
538
|
+
statusCode: numberEnv('SECURENOW_FIREWALL_STATUS_CODE', 403, 100),
|
|
539
|
+
log: boolEnv('SECURENOW_FIREWALL_LOG', true),
|
|
540
|
+
tcp: boolEnv('SECURENOW_FIREWALL_TCP', false),
|
|
541
|
+
iptables: boolEnv('SECURENOW_FIREWALL_IPTABLES', false),
|
|
542
|
+
cloud: env('SECURENOW_FIREWALL_CLOUD') || null,
|
|
543
|
+
cloudDryRun: boolEnv('SECURENOW_FIREWALL_CLOUD_DRY_RUN', false),
|
|
544
|
+
cloudflare: {
|
|
545
|
+
apiToken: env('CLOUDFLARE_API_TOKEN') || null,
|
|
546
|
+
accountId: env('CLOUDFLARE_ACCOUNT_ID') || null,
|
|
547
|
+
},
|
|
548
|
+
aws: {
|
|
549
|
+
wafIpSetId: env('AWS_WAF_IP_SET_ID') || null,
|
|
550
|
+
wafIpSetName: env('AWS_WAF_IP_SET_NAME') || 'securenow-blocklist',
|
|
551
|
+
wafScope: env('AWS_WAF_SCOPE') || 'REGIONAL',
|
|
552
|
+
},
|
|
553
|
+
gcp: {
|
|
554
|
+
projectId: env('GCP_PROJECT_ID') || null,
|
|
555
|
+
securityPolicy: env('GCP_SECURITY_POLICY') || null,
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
184
560
|
module.exports = {
|
|
185
561
|
FREE_TRIAL_INSTANCE,
|
|
562
|
+
DEFAULT_API_URL,
|
|
563
|
+
CONFIG_SCHEMA_VERSION,
|
|
564
|
+
DEFAULT_CONFIG,
|
|
565
|
+
CONFIG_EXPLANATIONS,
|
|
566
|
+
ENV_TO_CONFIG_PATH,
|
|
567
|
+
withCredentialDefaults,
|
|
568
|
+
mergeCredentials,
|
|
569
|
+
resolveConfigPath,
|
|
186
570
|
resolveAppKey,
|
|
187
571
|
resolveAppName,
|
|
188
572
|
resolveAppId,
|
|
189
573
|
resolveApiKey,
|
|
190
574
|
resolveInstance,
|
|
575
|
+
normalizeDeploymentEnvironment,
|
|
576
|
+
resolveDeploymentEnvironment,
|
|
191
577
|
resolveAll,
|
|
192
578
|
resolveNoUuid,
|
|
579
|
+
resolveOtlpHeaders,
|
|
580
|
+
resolveOtlpHeaderString,
|
|
581
|
+
resolveEndpoints,
|
|
582
|
+
resolveFirewallOptions,
|
|
583
|
+
env,
|
|
584
|
+
boolEnv,
|
|
585
|
+
numberEnv,
|
|
586
|
+
listEnv,
|
|
587
|
+
parseHeaders,
|
|
588
|
+
headersToString,
|
|
193
589
|
loadCredentials,
|
|
194
590
|
loadLocalCredentials,
|
|
195
591
|
loadGlobalCredentials,
|
package/cli/apiKey.js
CHANGED
|
@@ -45,7 +45,7 @@ async function clear(args, flags) {
|
|
|
45
45
|
async function show() {
|
|
46
46
|
const key = config.getApiKey();
|
|
47
47
|
if (!key) {
|
|
48
|
-
ui.info('No API key stored in credentials.
|
|
48
|
+
ui.info('No API key stored in credentials. Run `npx securenow login` or `npx securenow api-key set snk_live_...`.');
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
console.log(maskKey(key));
|
package/cli/apps.js
CHANGED
|
@@ -242,7 +242,7 @@ async function info(args, flags) {
|
|
|
242
242
|
|
|
243
243
|
console.log('');
|
|
244
244
|
console.log(` ${ui.c.bold('Use in current project:')} ${ui.c.bold(`securenow apps default ${app.key}`)}`);
|
|
245
|
-
console.log(` ${ui.c.bold('Or
|
|
245
|
+
console.log(` ${ui.c.bold('Or for CI/prod env:')} ${ui.c.dim(`SECURENOW_APPID=${app.key} SECURENOW_INSTANCE=${envUrl}`)}`);
|
|
246
246
|
console.log('');
|
|
247
247
|
} catch (err) {
|
|
248
248
|
s.fail('Failed to fetch application');
|