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/cli/config.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const appConfig = require('../app-config');
|
|
6
7
|
|
|
7
8
|
const CONFIG_DIR = path.join(os.homedir(), '.securenow');
|
|
8
9
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -44,6 +45,10 @@ function saveJSON(filepath, data) {
|
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function credentialsFileForLocal(local) {
|
|
49
|
+
return local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
function hasLocalCredentials() {
|
|
48
53
|
return fs.existsSync(LOCAL_CREDENTIALS_FILE);
|
|
49
54
|
}
|
|
@@ -86,12 +91,21 @@ function setConfigValue(key, value) {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
function loadCredentials() {
|
|
89
|
-
|
|
94
|
+
const global = loadJSON(CREDENTIALS_FILE);
|
|
95
|
+
const local = fs.existsSync(LOCAL_CREDENTIALS_FILE) ? loadJSON(LOCAL_CREDENTIALS_FILE) : null;
|
|
96
|
+
return appConfig.withCredentialDefaults(appConfig.mergeCredentials(global, local)) || {};
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
function saveCredentials(creds, { local = false } = {}) {
|
|
93
|
-
const targetFile = local
|
|
94
|
-
saveJSON(targetFile, creds);
|
|
100
|
+
const targetFile = credentialsFileForLocal(local);
|
|
101
|
+
saveJSON(targetFile, appConfig.withCredentialDefaults(creds) || {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ensureCredentialDefaults({ local } = {}) {
|
|
105
|
+
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
106
|
+
const targetFile = credentialsFileForLocal(useLocal);
|
|
107
|
+
const existing = loadJSON(targetFile);
|
|
108
|
+
saveJSON(targetFile, appConfig.withCredentialDefaults(existing || {}) || {});
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
function clearCredentials({ local } = {}) {
|
|
@@ -119,7 +133,8 @@ function getToken() {
|
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
function setAuth(token, email, expiresAt, { local = false, app = null } = {}) {
|
|
122
|
-
const
|
|
136
|
+
const targetFile = credentialsFileForLocal(local);
|
|
137
|
+
const payload = { ...loadJSON(targetFile), token, email, expiresAt };
|
|
123
138
|
if (app && (app.key || app.name || app.instance)) {
|
|
124
139
|
payload.app = {
|
|
125
140
|
key: app.key || null,
|
|
@@ -137,18 +152,18 @@ function getApp() {
|
|
|
137
152
|
|
|
138
153
|
function setApiKey(apiKey, { local } = {}) {
|
|
139
154
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
140
|
-
const targetFile = useLocal
|
|
155
|
+
const targetFile = credentialsFileForLocal(useLocal);
|
|
141
156
|
const existing = loadJSON(targetFile);
|
|
142
|
-
saveJSON(targetFile, { ...existing, apiKey });
|
|
157
|
+
saveJSON(targetFile, appConfig.withCredentialDefaults({ ...existing, apiKey }) || { apiKey });
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
function clearApiKey({ local } = {}) {
|
|
146
161
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
147
|
-
const targetFile = useLocal
|
|
162
|
+
const targetFile = credentialsFileForLocal(useLocal);
|
|
148
163
|
const existing = loadJSON(targetFile);
|
|
149
164
|
if (!existing || !existing.apiKey) return;
|
|
150
165
|
delete existing.apiKey;
|
|
151
|
-
saveJSON(targetFile, existing);
|
|
166
|
+
saveJSON(targetFile, appConfig.withCredentialDefaults(existing) || existing);
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
function getApiKey() {
|
|
@@ -158,16 +173,16 @@ function getApiKey() {
|
|
|
158
173
|
|
|
159
174
|
function setApp(app, { local } = {}) {
|
|
160
175
|
const useLocal = local === true || (local == null && hasLocalCredentials());
|
|
161
|
-
const targetFile = useLocal
|
|
176
|
+
const targetFile = credentialsFileForLocal(useLocal);
|
|
162
177
|
const existing = loadJSON(targetFile);
|
|
163
|
-
saveJSON(targetFile, {
|
|
178
|
+
saveJSON(targetFile, appConfig.withCredentialDefaults({
|
|
164
179
|
...existing,
|
|
165
180
|
app: {
|
|
166
181
|
key: app.key || null,
|
|
167
182
|
name: app.name || null,
|
|
168
183
|
instance: app.instance || null,
|
|
169
184
|
},
|
|
170
|
-
});
|
|
185
|
+
}) || {});
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
function ensureLocalGitignore() {
|
|
@@ -186,7 +201,10 @@ function ensureLocalGitignore() {
|
|
|
186
201
|
}
|
|
187
202
|
|
|
188
203
|
function getApiUrl() {
|
|
189
|
-
|
|
204
|
+
if (process.env.SECURENOW_API_URL) return process.env.SECURENOW_API_URL;
|
|
205
|
+
const cfg = loadConfig();
|
|
206
|
+
if (cfg.apiUrl && cfg.apiUrl !== DEFAULTS.apiUrl) return cfg.apiUrl;
|
|
207
|
+
return appConfig.env('SECURENOW_API_URL') || cfg.apiUrl;
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
function getAppUrl() {
|
|
@@ -219,6 +237,7 @@ module.exports = {
|
|
|
219
237
|
getApiKey,
|
|
220
238
|
getAuthSource,
|
|
221
239
|
hasLocalCredentials,
|
|
240
|
+
ensureCredentialDefaults,
|
|
222
241
|
ensureLocalGitignore,
|
|
223
242
|
getApiUrl,
|
|
224
243
|
getAppUrl,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const ui = require('./ui');
|
|
7
|
+
const appConfig = require('../app-config');
|
|
8
|
+
|
|
9
|
+
function maskSecret(value) {
|
|
10
|
+
if (!value) return '';
|
|
11
|
+
const text = String(value);
|
|
12
|
+
if (text.length <= 12) return '***';
|
|
13
|
+
return `${text.slice(0, 8)}...${text.slice(-4)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildRuntimeCredentials(options = {}) {
|
|
17
|
+
const creds = config.loadCredentials() || {};
|
|
18
|
+
const deploymentEnvironment =
|
|
19
|
+
options.environment ||
|
|
20
|
+
options.env ||
|
|
21
|
+
appConfig.resolveDeploymentEnvironment() ||
|
|
22
|
+
'production';
|
|
23
|
+
const runtime = appConfig.withCredentialDefaults({
|
|
24
|
+
apiKey: creds.apiKey || null,
|
|
25
|
+
app: {
|
|
26
|
+
key: creds.app?.key || null,
|
|
27
|
+
name: creds.app?.name || null,
|
|
28
|
+
instance: creds.app?.instance || appConfig.FREE_TRIAL_INSTANCE,
|
|
29
|
+
},
|
|
30
|
+
config: {
|
|
31
|
+
...(creds.config || {}),
|
|
32
|
+
runtime: {
|
|
33
|
+
...(creds.config?.runtime || {}),
|
|
34
|
+
deploymentEnvironment,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
_securenow: {
|
|
38
|
+
...(creds._securenow || {}),
|
|
39
|
+
note: 'Runtime SecureNow credentials and SDK defaults. Mount or copy this JSON as .securenow/credentials.json in production. Do not commit it.',
|
|
40
|
+
runtimeOnly: 'This file intentionally omits CLI OAuth fields: token, email, and expiresAt.',
|
|
41
|
+
production: 'Production can use this same file shape instead of environment variables.',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
delete runtime.token;
|
|
46
|
+
delete runtime.email;
|
|
47
|
+
delete runtime.expiresAt;
|
|
48
|
+
return runtime;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeFileSafe(filepath, data) {
|
|
52
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true, mode: 0o700 });
|
|
53
|
+
fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runtime(_args, flags) {
|
|
57
|
+
const creds = buildRuntimeCredentials(flags);
|
|
58
|
+
const envName = creds.config?.runtime?.deploymentEnvironment || 'production';
|
|
59
|
+
const defaultOutput = envName === 'production'
|
|
60
|
+
? path.join('.securenow', 'credentials.production.json')
|
|
61
|
+
: path.join('.securenow', `credentials.${envName}.json`);
|
|
62
|
+
const output = flags.out || flags.output || defaultOutput;
|
|
63
|
+
const warn = flags.stdout ? (msg) => console.error(`! ${msg}`) : ui.warn;
|
|
64
|
+
|
|
65
|
+
if (!creds.app || !creds.app.key) {
|
|
66
|
+
warn('No app key found. Run `npx securenow login` first so telemetry routes to the selected app.');
|
|
67
|
+
}
|
|
68
|
+
if (!creds.apiKey) {
|
|
69
|
+
warn('No firewall API key found. Run `npx securenow login` or `npx securenow api-key set snk_live_...` first.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (flags.stdout) {
|
|
73
|
+
console.log(JSON.stringify(creds, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
writeFileSafe(path.resolve(process.cwd(), output), creds);
|
|
78
|
+
config.ensureLocalGitignore();
|
|
79
|
+
|
|
80
|
+
ui.success(`Wrote runtime credentials to ${output}`);
|
|
81
|
+
ui.info('Deploy this JSON as .securenow/credentials.json on the server/container.');
|
|
82
|
+
ui.info(`Environment: ${envName}`);
|
|
83
|
+
ui.info(`App: ${creds.app?.name || '(unnamed)'} ${creds.app?.key ? `(${creds.app.key})` : ''}`);
|
|
84
|
+
ui.info(`Firewall key: ${creds.apiKey ? maskSecret(creds.apiKey) : '(missing)'}`);
|
|
85
|
+
ui.info('OAuth token/email were not included.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { runtime, buildRuntimeCredentials };
|
package/cli/diagnostics.js
CHANGED
|
@@ -4,62 +4,43 @@ const crypto = require('crypto');
|
|
|
4
4
|
const url = require('url');
|
|
5
5
|
const ui = require('./ui');
|
|
6
6
|
const config = require('./config');
|
|
7
|
+
const appConfig = require('../app-config');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function resolvedConfig() {
|
|
11
|
-
// Mirror the SDK's resolution: env → credentials file → package.json#name.
|
|
12
|
-
// Doing it here means `securenow doctor` reports what the SDK will actually
|
|
13
|
-
// send — critical for diagnosing "my traces don't show up" (the common
|
|
14
|
-
// cause is a service.name mismatch with the dashboard's exact-match filter).
|
|
15
|
-
const appConfig = require('../app-config');
|
|
9
|
+
function resolvedConfig(options = {}) {
|
|
16
10
|
const resolvedApp = appConfig.resolveAll();
|
|
11
|
+
const endpoints = appConfig.resolveEndpoints();
|
|
12
|
+
const firewall = appConfig.resolveFirewallOptions();
|
|
17
13
|
const noUuid = appConfig.resolveNoUuid();
|
|
14
|
+
const deploymentEnvironment = appConfig.normalizeDeploymentEnvironment(
|
|
15
|
+
options.environment || options.env || resolvedApp.deploymentEnvironment
|
|
16
|
+
);
|
|
18
17
|
|
|
19
|
-
const baseName = (
|
|
18
|
+
const baseName = (resolvedApp.appId || '').trim().replace(/^['"]|['"]$/g, '') || null;
|
|
20
19
|
const serviceName = baseName
|
|
21
20
|
? (noUuid ? baseName : `${baseName}-<uuid-per-worker>`)
|
|
22
21
|
: '(auto-generated)';
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const tracesEndpoint =
|
|
26
|
-
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
|
|
27
|
-
(process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
28
|
-
? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/traces`
|
|
29
|
-
: `${instance.replace(/\/$/, '')}/v1/traces`);
|
|
30
|
-
const logsEndpoint =
|
|
31
|
-
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ||
|
|
32
|
-
(process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
33
|
-
? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/logs`
|
|
34
|
-
: `${instance.replace(/\/$/, '')}/v1/logs`);
|
|
35
|
-
const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
|
|
36
|
-
// Resolve firewall API key the same way the SDK does: env, then
|
|
37
|
-
// .securenow/credentials.json (project-local, then global).
|
|
38
|
-
const apiKey = require('../app-config').resolveApiKey() || '';
|
|
39
|
-
const apiUrl = config.getApiUrl();
|
|
40
|
-
const loggingEnabled = !/^(0|false)$/i.test(String(process.env.SECURENOW_LOGGING_ENABLED ?? ''));
|
|
41
|
-
const captureBody = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_BODY ?? ''));
|
|
42
|
-
const captureMultipart = !/^(0|false)$/i.test(String(process.env.SECURENOW_CAPTURE_MULTIPART ?? ''));
|
|
43
|
-
const firewallEnabled =
|
|
44
|
-
!!apiKey && process.env.SECURENOW_FIREWALL_ENABLED !== '0';
|
|
22
|
+
const apiKey = firewall.apiKey || '';
|
|
23
|
+
const firewallEnabled = !!apiKey && firewall.enabled;
|
|
45
24
|
|
|
46
25
|
return {
|
|
47
26
|
serviceName,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
appKey: resolvedApp.appKey || null,
|
|
28
|
+
deploymentEnvironment,
|
|
29
|
+
instance: endpoints.endpointBase,
|
|
30
|
+
tracesEndpoint: endpoints.tracesUrl,
|
|
31
|
+
logsEndpoint: endpoints.logsUrl,
|
|
32
|
+
headers: endpoints.headers,
|
|
52
33
|
apiKey,
|
|
53
|
-
apiUrl,
|
|
54
|
-
loggingEnabled,
|
|
55
|
-
captureBody,
|
|
56
|
-
captureMultipart,
|
|
34
|
+
apiUrl: config.getApiUrl(),
|
|
35
|
+
loggingEnabled: appConfig.boolEnv('SECURENOW_LOGGING_ENABLED', true),
|
|
36
|
+
captureBody: appConfig.boolEnv('SECURENOW_CAPTURE_BODY', true),
|
|
37
|
+
captureMultipart: appConfig.boolEnv('SECURENOW_CAPTURE_MULTIPART', true),
|
|
57
38
|
firewallEnabled,
|
|
58
39
|
firewallLayers: {
|
|
59
40
|
http: firewallEnabled,
|
|
60
|
-
tcp: firewallEnabled &&
|
|
61
|
-
iptables: firewallEnabled &&
|
|
62
|
-
cloud: firewallEnabled ?
|
|
41
|
+
tcp: firewallEnabled && firewall.tcp,
|
|
42
|
+
iptables: firewallEnabled && firewall.iptables,
|
|
43
|
+
cloud: firewallEnabled ? firewall.cloud : null,
|
|
63
44
|
},
|
|
64
45
|
};
|
|
65
46
|
}
|
|
@@ -74,24 +55,11 @@ function maskSecret(value) {
|
|
|
74
55
|
function redactConfig(cfg) {
|
|
75
56
|
return {
|
|
76
57
|
...cfg,
|
|
77
|
-
headers: cfg.headers ? '***' : '',
|
|
58
|
+
headers: cfg.headers && Object.keys(cfg.headers).length ? '***' : '',
|
|
78
59
|
apiKey: cfg.apiKey ? maskSecret(cfg.apiKey) : '',
|
|
79
60
|
};
|
|
80
61
|
}
|
|
81
62
|
|
|
82
|
-
function parseHeaders(str) {
|
|
83
|
-
const out = {};
|
|
84
|
-
if (!str) return out;
|
|
85
|
-
for (const pair of str.split(',')) {
|
|
86
|
-
const [k, ...rest] = pair.split('=');
|
|
87
|
-
if (!k) continue;
|
|
88
|
-
out[k.trim()] = rest.join('=').trim();
|
|
89
|
-
}
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── HTTP helpers ──
|
|
94
|
-
|
|
95
63
|
function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs = 5000 }) {
|
|
96
64
|
return new Promise((resolve, reject) => {
|
|
97
65
|
const parsed = new url.URL(endpoint);
|
|
@@ -121,8 +89,6 @@ function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs
|
|
|
121
89
|
});
|
|
122
90
|
}
|
|
123
91
|
|
|
124
|
-
// ── OTLP/HTTP JSON payloads ──
|
|
125
|
-
|
|
126
92
|
function attr(key, value) {
|
|
127
93
|
if (typeof value === 'number') {
|
|
128
94
|
if (Number.isInteger(value)) return { key, value: { intValue: String(value) } };
|
|
@@ -135,7 +101,7 @@ function attr(key, value) {
|
|
|
135
101
|
function resourceAttrs(cfg, extra = {}) {
|
|
136
102
|
const base = {
|
|
137
103
|
'service.name': cfg.serviceName,
|
|
138
|
-
'deployment.environment':
|
|
104
|
+
'deployment.environment': cfg.deploymentEnvironment || appConfig.resolveDeploymentEnvironment(),
|
|
139
105
|
'telemetry.sdk.name': 'securenow-cli',
|
|
140
106
|
'telemetry.sdk.language': 'nodejs',
|
|
141
107
|
...extra,
|
|
@@ -201,18 +167,19 @@ function buildLogPayload(cfg, { message, level = 'info', attributes = {} }) {
|
|
|
201
167
|
};
|
|
202
168
|
}
|
|
203
169
|
|
|
204
|
-
// ── Commands ──
|
|
205
|
-
|
|
206
170
|
async function testSpan(args, flags) {
|
|
207
|
-
const cfg = resolvedConfig();
|
|
171
|
+
const cfg = resolvedConfig(flags);
|
|
208
172
|
const spanName = args[0] || 'securenow.cli.test-span';
|
|
209
173
|
const headers = {
|
|
210
174
|
'Content-Type': 'application/json',
|
|
211
|
-
...
|
|
175
|
+
...cfg.headers,
|
|
212
176
|
};
|
|
213
177
|
const payload = buildTracePayload(cfg, {
|
|
214
178
|
name: spanName,
|
|
215
|
-
attributes: {
|
|
179
|
+
attributes: {
|
|
180
|
+
'test.source': 'securenow-cli',
|
|
181
|
+
'securenow.environment': cfg.deploymentEnvironment,
|
|
182
|
+
},
|
|
216
183
|
});
|
|
217
184
|
|
|
218
185
|
const spin = ui.spinner(`Sending test span to ${cfg.tracesEndpoint}`);
|
|
@@ -224,7 +191,7 @@ async function testSpan(args, flags) {
|
|
|
224
191
|
});
|
|
225
192
|
if (res.status >= 200 && res.status < 300) {
|
|
226
193
|
spin.stop(`Span accepted (HTTP ${res.status})`);
|
|
227
|
-
if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.tracesEndpoint });
|
|
194
|
+
if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.tracesEndpoint, environment: cfg.deploymentEnvironment });
|
|
228
195
|
return;
|
|
229
196
|
}
|
|
230
197
|
spin.fail(`Collector returned HTTP ${res.status}`);
|
|
@@ -246,9 +213,9 @@ async function logSend(args, flags) {
|
|
|
246
213
|
process.exit(1);
|
|
247
214
|
}
|
|
248
215
|
|
|
249
|
-
const cfg = resolvedConfig();
|
|
216
|
+
const cfg = resolvedConfig(flags);
|
|
250
217
|
if (!cfg.loggingEnabled) {
|
|
251
|
-
ui.warn('Logging is disabled
|
|
218
|
+
ui.warn('Logging is disabled in SecureNow credentials/config. Sending anyway.');
|
|
252
219
|
}
|
|
253
220
|
|
|
254
221
|
const level = (flags.level || 'info').toString();
|
|
@@ -262,9 +229,16 @@ async function logSend(args, flags) {
|
|
|
262
229
|
|
|
263
230
|
const headers = {
|
|
264
231
|
'Content-Type': 'application/json',
|
|
265
|
-
...
|
|
232
|
+
...cfg.headers,
|
|
266
233
|
};
|
|
267
|
-
const payload = buildLogPayload(cfg, {
|
|
234
|
+
const payload = buildLogPayload(cfg, {
|
|
235
|
+
message,
|
|
236
|
+
level,
|
|
237
|
+
attributes: {
|
|
238
|
+
...attributes,
|
|
239
|
+
'securenow.environment': cfg.deploymentEnvironment,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
268
242
|
|
|
269
243
|
const spin = ui.spinner(`Sending log to ${cfg.logsEndpoint}`);
|
|
270
244
|
try {
|
|
@@ -275,7 +249,7 @@ async function logSend(args, flags) {
|
|
|
275
249
|
});
|
|
276
250
|
if (res.status >= 200 && res.status < 300) {
|
|
277
251
|
spin.stop(`Log accepted (HTTP ${res.status})`);
|
|
278
|
-
if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.logsEndpoint });
|
|
252
|
+
if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.logsEndpoint, environment: cfg.deploymentEnvironment });
|
|
279
253
|
return;
|
|
280
254
|
}
|
|
281
255
|
spin.fail(`Collector returned HTTP ${res.status}`);
|
|
@@ -289,12 +263,8 @@ async function logSend(args, flags) {
|
|
|
289
263
|
}
|
|
290
264
|
}
|
|
291
265
|
|
|
292
|
-
// ── doctor / env ──
|
|
293
|
-
|
|
294
266
|
async function probe(endpoint, timeoutMs = 3000) {
|
|
295
267
|
try {
|
|
296
|
-
// A HEAD or empty POST is safer than sending real OTLP. Most collectors
|
|
297
|
-
// return 400/405 for a malformed request — that still proves reachability.
|
|
298
268
|
const res = await httpRequest({
|
|
299
269
|
method: 'POST',
|
|
300
270
|
endpoint,
|
|
@@ -310,34 +280,32 @@ async function probe(endpoint, timeoutMs = 3000) {
|
|
|
310
280
|
|
|
311
281
|
function env(_args, flags) {
|
|
312
282
|
const cfg = resolvedConfig();
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
SECURENOW_FIREWALL_IPTABLES: process.env.SECURENOW_FIREWALL_IPTABLES || '0',
|
|
329
|
-
SECURENOW_FIREWALL_CLOUD: process.env.SECURENOW_FIREWALL_CLOUD || null,
|
|
330
|
-
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
283
|
+
const resolved = {
|
|
284
|
+
appKey: cfg.appKey,
|
|
285
|
+
serviceName: cfg.serviceName,
|
|
286
|
+
deploymentEnvironment: cfg.deploymentEnvironment,
|
|
287
|
+
instance: cfg.instance,
|
|
288
|
+
tracesEndpoint: cfg.tracesEndpoint,
|
|
289
|
+
logsEndpoint: cfg.logsEndpoint,
|
|
290
|
+
otlpHeaders: Object.keys(cfg.headers || {}).length ? '***' : null,
|
|
291
|
+
firewallApiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
|
|
292
|
+
apiUrl: cfg.apiUrl,
|
|
293
|
+
loggingEnabled: cfg.loggingEnabled,
|
|
294
|
+
captureBody: cfg.captureBody,
|
|
295
|
+
captureMultipart: cfg.captureMultipart,
|
|
296
|
+
noUuid: appConfig.resolveNoUuid(),
|
|
297
|
+
firewallLayers: cfg.firewallLayers,
|
|
331
298
|
};
|
|
332
299
|
|
|
333
300
|
if (flags.json) {
|
|
334
|
-
ui.json({ resolved: redactConfig(cfg),
|
|
301
|
+
ui.json({ resolved: redactConfig(cfg), config: resolved });
|
|
335
302
|
return;
|
|
336
303
|
}
|
|
337
304
|
|
|
338
305
|
ui.heading('Resolved configuration');
|
|
339
306
|
ui.keyValue([
|
|
340
307
|
['Service name', cfg.serviceName],
|
|
308
|
+
['Environment', cfg.deploymentEnvironment],
|
|
341
309
|
['Traces endpoint', cfg.tracesEndpoint],
|
|
342
310
|
['Logs endpoint', cfg.logsEndpoint],
|
|
343
311
|
['Logging', cfg.loggingEnabled ? ui.c.green('enabled') : ui.c.dim('disabled')],
|
|
@@ -346,9 +314,9 @@ function env(_args, flags) {
|
|
|
346
314
|
['Firewall', cfg.firewallEnabled ? ui.c.green('enabled') : ui.c.dim('disabled (no API key)')],
|
|
347
315
|
]);
|
|
348
316
|
|
|
349
|
-
ui.heading('
|
|
317
|
+
ui.heading('Resolved credentials');
|
|
350
318
|
ui.keyValue(
|
|
351
|
-
Object.entries(
|
|
319
|
+
Object.entries(resolved).map(([k, v]) => [k, v == null ? ui.c.dim('(not set)') : typeof v === 'object' ? JSON.stringify(v) : String(v)])
|
|
352
320
|
);
|
|
353
321
|
console.log('');
|
|
354
322
|
}
|
|
@@ -357,21 +325,18 @@ async function doctor(_args, flags) {
|
|
|
357
325
|
const cfg = resolvedConfig();
|
|
358
326
|
const checks = [];
|
|
359
327
|
|
|
360
|
-
// Collector traces
|
|
361
328
|
const spin1 = ui.spinner(`Probing traces endpoint ${cfg.tracesEndpoint}`);
|
|
362
329
|
const traces = await probe(cfg.tracesEndpoint);
|
|
363
330
|
if (traces.ok) spin1.stop(`Traces endpoint reachable (HTTP ${traces.status})`);
|
|
364
331
|
else spin1.fail(`Traces endpoint unreachable: ${traces.error}`);
|
|
365
332
|
checks.push({ name: 'traces', ...traces });
|
|
366
333
|
|
|
367
|
-
// Collector logs
|
|
368
334
|
const spin2 = ui.spinner(`Probing logs endpoint ${cfg.logsEndpoint}`);
|
|
369
335
|
const logs = await probe(cfg.logsEndpoint);
|
|
370
336
|
if (logs.ok) spin2.stop(`Logs endpoint reachable (HTTP ${logs.status})`);
|
|
371
337
|
else spin2.fail(`Logs endpoint unreachable: ${logs.error}`);
|
|
372
338
|
checks.push({ name: 'logs', ...logs });
|
|
373
339
|
|
|
374
|
-
// SecureNow API (only if API key or logged-in token)
|
|
375
340
|
const token = config.getToken();
|
|
376
341
|
if (cfg.apiKey || token) {
|
|
377
342
|
const spin3 = ui.spinner(`Probing SecureNow API ${cfg.apiUrl}`);
|
|
@@ -381,16 +346,15 @@ async function doctor(_args, flags) {
|
|
|
381
346
|
checks.push({ name: 'api', ...api });
|
|
382
347
|
}
|
|
383
348
|
|
|
384
|
-
// Config sanity
|
|
385
349
|
const warnings = [];
|
|
386
|
-
if (!
|
|
387
|
-
warnings.push('No
|
|
350
|
+
if (!cfg.appKey) {
|
|
351
|
+
warnings.push('No app key resolved. Run `npx securenow login` or set app.key in .securenow/credentials.json.');
|
|
388
352
|
}
|
|
389
353
|
if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
|
|
390
|
-
warnings.push('Using the free-trial collector
|
|
354
|
+
warnings.push('Using the free-trial collector. For production, set app.instance in .securenow/credentials.json.');
|
|
391
355
|
}
|
|
392
|
-
if (!cfg.apiKey
|
|
393
|
-
warnings.push('
|
|
356
|
+
if (!cfg.apiKey) {
|
|
357
|
+
warnings.push('No snk_live firewall API key is resolvable. Run `npx securenow login` or `npx securenow api-key set`.');
|
|
394
358
|
}
|
|
395
359
|
|
|
396
360
|
const ok = checks.every((c) => c.ok);
|
package/cli/firewall.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { api, requireAuth } = require('./client');
|
|
4
|
-
const ui = require('./ui');
|
|
3
|
+
const { api, requireAuth } = require('./client');
|
|
4
|
+
const ui = require('./ui');
|
|
5
|
+
|
|
6
|
+
function resolveEnvironment(flags) {
|
|
7
|
+
return flags.env || flags.environment || 'production';
|
|
8
|
+
}
|
|
5
9
|
|
|
6
10
|
async function status(args, flags) {
|
|
7
11
|
requireAuth();
|
|
8
12
|
const s = ui.spinner('Checking firewall status');
|
|
9
13
|
|
|
10
14
|
try {
|
|
11
|
-
const
|
|
15
|
+
const environment = resolveEnvironment(flags);
|
|
16
|
+
const data = await api.get('/firewall/status', { query: { environment } });
|
|
12
17
|
|
|
13
18
|
s.stop('Firewall status retrieved');
|
|
14
19
|
|
|
@@ -21,7 +26,8 @@ async function status(args, flags) {
|
|
|
21
26
|
console.log(` ${ui.c.bold(ui.c.green('Firewall: ENABLED'))}`);
|
|
22
27
|
console.log('');
|
|
23
28
|
ui.keyValue([
|
|
24
|
-
['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
|
|
29
|
+
['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
|
|
30
|
+
['Environment', data.environment || environment],
|
|
25
31
|
['Last updated', data.updatedAt || 'unknown'],
|
|
26
32
|
['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
|
|
27
33
|
['Allowlist updated', data.allowlistUpdatedAt || 'never'],
|
|
@@ -59,7 +65,8 @@ async function testIp(args, flags) {
|
|
|
59
65
|
const s = ui.spinner(`Testing IP ${ip}`);
|
|
60
66
|
|
|
61
67
|
try {
|
|
62
|
-
const
|
|
68
|
+
const environment = resolveEnvironment(flags);
|
|
69
|
+
const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query: { environment } });
|
|
63
70
|
|
|
64
71
|
s.stop(`IP ${ip} checked`);
|
|
65
72
|
|
|
@@ -115,10 +122,11 @@ async function setEnabled(args, flags, enabled) {
|
|
|
115
122
|
process.exit(1);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
const environment = resolveEnvironment(flags);
|
|
126
|
+
const verb = enabled ? 'Enabling' : 'Disabling';
|
|
127
|
+
const s = ui.spinner(`${verb} firewall for ${appKey} (${environment})`);
|
|
128
|
+
try {
|
|
129
|
+
const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled, environment });
|
|
122
130
|
s.stop(`Firewall ${enabled ? 'enabled' : 'disabled'}`);
|
|
123
131
|
|
|
124
132
|
if (flags.json) { ui.json(data); return; }
|
|
@@ -126,7 +134,8 @@ async function setEnabled(args, flags, enabled) {
|
|
|
126
134
|
const app = data.app || {};
|
|
127
135
|
console.log('');
|
|
128
136
|
console.log(` ${ui.c.bold(enabled ? ui.c.green('Firewall: ENABLED') : ui.c.yellow('Firewall: DISABLED'))} — ${app.name || appKey}`);
|
|
129
|
-
console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
|
|
137
|
+
console.log(` ${ui.c.dim('App key:')} ${app.key || appKey}`);
|
|
138
|
+
console.log(` ${ui.c.dim('Environment:')} ${app.environment || environment}`);
|
|
130
139
|
console.log('');
|
|
131
140
|
console.log(` ${ui.c.dim('Running SDK instances pick up the change within ~10s.')}`);
|
|
132
141
|
console.log('');
|
|
@@ -157,10 +166,16 @@ async function appsList(args, flags) {
|
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
console.log('');
|
|
160
|
-
for (const a of apps) {
|
|
161
|
-
const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
|
|
162
|
-
console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
|
|
163
|
-
|
|
169
|
+
for (const a of apps) {
|
|
170
|
+
const tag = a.firewallEnabled ? ui.c.green('ON ') : ui.c.yellow('OFF');
|
|
171
|
+
console.log(` ${tag} ${ui.c.bold(a.name)} ${ui.c.dim(a.key)}`);
|
|
172
|
+
if (a.environments) {
|
|
173
|
+
const envLine = Object.entries(a.environments)
|
|
174
|
+
.map(([env, state]) => `${env}:${state.firewallEnabled ? 'on' : 'off'}`)
|
|
175
|
+
.join(' ');
|
|
176
|
+
if (envLine) console.log(` ${ui.c.dim(envLine)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
164
179
|
console.log('');
|
|
165
180
|
console.log(` ${ui.c.dim('Toggle:')} securenow firewall enable --app <key>`);
|
|
166
181
|
console.log(` ${ui.c.dim(' :')} securenow firewall disable --app <key>`);
|