securenow 7.2.1 → 7.4.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/cli/auth.js +14 -2
- package/cli/firewall.js +75 -1
- package/cli.js +6 -2
- package/docs/ENVIRONMENT-VARIABLES.md +1 -1
- package/firewall-only.js +6 -1
- package/firewall.js +42 -5
- package/nextjs.js +4 -1
- package/nuxt-server-plugin.mjs +2 -0
- package/package.json +1 -1
- package/tracing.js +6 -2
package/cli/auth.js
CHANGED
|
@@ -19,6 +19,18 @@ function openBrowser(url) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function buildCliAuthUrl(appUrl, port, nonce, extra = {}) {
|
|
23
|
+
const url = new URL('/cli/auth', appUrl);
|
|
24
|
+
url.searchParams.set('callback', `http://127.0.0.1:${port}/callback`);
|
|
25
|
+
url.searchParams.set('state', nonce);
|
|
26
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
27
|
+
if (value !== undefined && value !== null) {
|
|
28
|
+
url.searchParams.set(key, String(value));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return url.toString();
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
function decodeJwtPayload(token) {
|
|
23
35
|
try {
|
|
24
36
|
const parts = token.split('.');
|
|
@@ -98,7 +110,7 @@ async function loginWithBrowser() {
|
|
|
98
110
|
const email = payload?.email || 'unknown account';
|
|
99
111
|
const safeEmail = email.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
100
112
|
const port = server.address().port;
|
|
101
|
-
const switchUrl =
|
|
113
|
+
const switchUrl = buildCliAuthUrl(appUrl, port, nonce, { force_login: 1 });
|
|
102
114
|
|
|
103
115
|
res.end([
|
|
104
116
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>SecureNow CLI Login</title></head>',
|
|
@@ -171,7 +183,7 @@ async function loginWithBrowser() {
|
|
|
171
183
|
|
|
172
184
|
server.listen(0, '127.0.0.1', () => {
|
|
173
185
|
const port = server.address().port;
|
|
174
|
-
const authUrl =
|
|
186
|
+
const authUrl = buildCliAuthUrl(appUrl, port, nonce);
|
|
175
187
|
|
|
176
188
|
console.log('');
|
|
177
189
|
ui.info('Opening browser for authentication...');
|
package/cli/firewall.js
CHANGED
|
@@ -97,4 +97,78 @@ async function testIp(args, flags) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
async function resolveAppKey(flags) {
|
|
101
|
+
if (flags && flags.app) return String(flags.app);
|
|
102
|
+
try {
|
|
103
|
+
const { resolveAppKey: r } = require('../app-config');
|
|
104
|
+
const k = r();
|
|
105
|
+
if (k) return k;
|
|
106
|
+
} catch (_) {}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function setEnabled(args, flags, enabled) {
|
|
111
|
+
requireAuth();
|
|
112
|
+
const appKey = await resolveAppKey(flags);
|
|
113
|
+
if (!appKey) {
|
|
114
|
+
ui.error('No app selected. Pass --app <key> or run `securenow login` / `securenow apps default <key>`.');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const verb = enabled ? 'Enabling' : 'Disabling';
|
|
119
|
+
const s = ui.spinner(`${verb} firewall for ${appKey}`);
|
|
120
|
+
try {
|
|
121
|
+
const data = await api.patch(`/firewall/app/${encodeURIComponent(appKey)}`, { enabled });
|
|
122
|
+
s.stop(`Firewall ${enabled ? 'enabled' : 'disabled'}`);
|
|
123
|
+
|
|
124
|
+
if (flags.json) { ui.json(data); return; }
|
|
125
|
+
|
|
126
|
+
const app = data.app || {};
|
|
127
|
+
console.log('');
|
|
128
|
+
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}`);
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(` ${ui.c.dim('Running SDK instances pick up the change within ~10s.')}`);
|
|
132
|
+
console.log('');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
s.fail(`Failed to ${enabled ? 'enable' : 'disable'} firewall`);
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function enable(args, flags) { return setEnabled(args, flags, true); }
|
|
140
|
+
async function disable(args, flags) { return setEnabled(args, flags, false); }
|
|
141
|
+
|
|
142
|
+
async function appsList(args, flags) {
|
|
143
|
+
requireAuth();
|
|
144
|
+
const s = ui.spinner('Loading apps');
|
|
145
|
+
try {
|
|
146
|
+
const data = await api.get('/firewall/apps');
|
|
147
|
+
s.stop('Apps loaded');
|
|
148
|
+
|
|
149
|
+
if (flags.json) { ui.json(data); return; }
|
|
150
|
+
|
|
151
|
+
const apps = data.apps || [];
|
|
152
|
+
if (apps.length === 0) {
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(` ${ui.c.dim('No apps yet. Create one with `securenow apps create <name>`.')}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
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
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(` ${ui.c.dim('Toggle:')} securenow firewall enable --app <key>`);
|
|
166
|
+
console.log(` ${ui.c.dim(' :')} securenow firewall disable --app <key>`);
|
|
167
|
+
console.log('');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
s.fail('Failed to list apps');
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { status, testIp, enable, disable, appsList };
|
package/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const ui = require('./cli/ui');
|
|
@@ -178,10 +178,14 @@ const COMMANDS = {
|
|
|
178
178
|
defaultSub: 'list',
|
|
179
179
|
},
|
|
180
180
|
firewall: {
|
|
181
|
-
desc: 'Firewall status and IP testing',
|
|
181
|
+
desc: 'Firewall status, per-app toggle, and IP testing',
|
|
182
182
|
usage: 'securenow firewall <subcommand> [options]',
|
|
183
|
+
flags: { app: 'App key (defaults to logged-in app)', json: 'Output as JSON' },
|
|
183
184
|
sub: {
|
|
184
185
|
status: { desc: 'Show firewall status, layers, and blocklist info', run: (a, f) => require('./cli/firewall').status(a, f) },
|
|
186
|
+
apps: { desc: 'List apps with their firewall on/off state', run: (a, f) => require('./cli/firewall').appsList(a, f) },
|
|
187
|
+
enable: { desc: 'Turn the firewall ON for an app', usage: 'securenow firewall enable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').enable(a, f) },
|
|
188
|
+
disable: { desc: 'Turn the firewall OFF for an app', usage: 'securenow firewall disable [--app <key>]', flags: { app: 'App key (defaults to logged-in app)' }, run: (a, f) => require('./cli/firewall').disable(a, f) },
|
|
185
189
|
'test-ip': { desc: 'Check if an IP would be blocked', usage: 'securenow firewall test-ip <ip>', run: (a, f) => require('./cli/firewall').testIp(a, f) },
|
|
186
190
|
},
|
|
187
191
|
defaultSub: 'status',
|
|
@@ -25,7 +25,7 @@ Complete reference for all environment variables supported by SecureNow.
|
|
|
25
25
|
| **SECURENOW_DISABLE_INSTRUMENTATIONS** | Optional | - | Comma-separated list of OTel instrumentations to disable |
|
|
26
26
|
| **SECURENOW_TEST_SPAN** | Optional | `0` | Emit a single test span on startup (prefer `npx securenow test-span`) |
|
|
27
27
|
| **SECURENOW_HIDE_BANNER** | Optional | `0` | Hide the free-trial banner |
|
|
28
|
-
| **SECURENOW_FIREWALL_ENABLED** | Optional | `
|
|
28
|
+
| **SECURENOW_FIREWALL_ENABLED** | Optional | (dashboard toggle) | Local override only — set to `0` to force-off regardless of dashboard. Primary control is the per-app toggle at `/dashboard/firewall` (≥ 7.3.0). |
|
|
29
29
|
| **SECURENOW_ENABLE_MONGODB_INSTRUMENTATION** | Optional | `0` | Opt in to MongoDB instrumentation (off by default since a cursor bug on mongodb@6.6+; safe since SDK v6.0.2) |
|
|
30
30
|
| **OTEL_SERVICE_NAME** | Optional | - | Alternative to SECURENOW_APPID (label only, no routing) |
|
|
31
31
|
| **OTEL_EXPORTER_OTLP_ENDPOINT** | Optional | - | Alternative to SECURENOW_INSTANCE |
|
package/firewall-only.js
CHANGED
|
@@ -16,12 +16,17 @@ try { require('dotenv').config(); } catch (_) {}
|
|
|
16
16
|
const env = (k) =>
|
|
17
17
|
process.env[k] ?? process.env[k.toUpperCase()] ?? process.env[k.toLowerCase()];
|
|
18
18
|
|
|
19
|
-
const { resolveApiKey } = require('./app-config');
|
|
19
|
+
const { resolveApiKey, resolveAppKey } = require('./app-config');
|
|
20
20
|
const firewallApiKey = resolveApiKey();
|
|
21
|
+
const appKey = resolveAppKey();
|
|
21
22
|
|
|
23
|
+
// SECURENOW_FIREWALL_ENABLED=0 is a hard local override (ops escape hatch).
|
|
24
|
+
// In all other cases the toggle lives in the dashboard; default ON when an
|
|
25
|
+
// API key is present.
|
|
22
26
|
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
23
27
|
require('./firewall').init({
|
|
24
28
|
apiKey: firewallApiKey,
|
|
29
|
+
appKey: appKey || null,
|
|
25
30
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
26
31
|
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
27
32
|
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
package/firewall.js
CHANGED
|
@@ -16,9 +16,16 @@ let _initialized = false;
|
|
|
16
16
|
let _consecutiveErrors = 0;
|
|
17
17
|
let _layers = [];
|
|
18
18
|
let _rawIps = [];
|
|
19
|
-
let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0 };
|
|
19
|
+
let _stats = { syncs: 0, blocked: 0, allowed: 0, versionChecks: 0, errors: 0, suppressedDisabled: 0 };
|
|
20
20
|
let _localhostFallbackTried = false;
|
|
21
21
|
|
|
22
|
+
// Remote toggle — set by /firewall/sync when an appKey is in scope. Default
|
|
23
|
+
// true so a missing/unreachable backend fails open (matches pre-7.3 behavior).
|
|
24
|
+
// When the dashboard / CLI flips this off, the next poll suppresses
|
|
25
|
+
// enforcement without restarting the host process.
|
|
26
|
+
let _remoteEnabled = true;
|
|
27
|
+
let _lastRemoteEnabled = null;
|
|
28
|
+
|
|
22
29
|
// Allowlist state
|
|
23
30
|
let _allowlistMatcher = null;
|
|
24
31
|
let _allowlistRawIps = [];
|
|
@@ -133,7 +140,8 @@ function httpGet(url, extraHeaders, timeout, callback) {
|
|
|
133
140
|
// ────── Unified Sync (v2 — single request for everything) ──────
|
|
134
141
|
|
|
135
142
|
function doUnifiedSync(callback) {
|
|
136
|
-
const
|
|
143
|
+
const appQuery = _options.appKey ? `?app=${encodeURIComponent(_options.appKey)}` : '';
|
|
144
|
+
const url = buildUrl(_options.apiUrl, '/firewall/sync') + appQuery;
|
|
137
145
|
const headers = {};
|
|
138
146
|
if (_lastVersion) headers['X-Blocklist-Version'] = _lastVersion;
|
|
139
147
|
if (_lastAllowlistVersion) headers['X-Allowlist-Version'] = _lastAllowlistVersion;
|
|
@@ -165,6 +173,21 @@ function doUnifiedSync(callback) {
|
|
|
165
173
|
let blChanged = false;
|
|
166
174
|
let alChanged = false;
|
|
167
175
|
|
|
176
|
+
// Apply remote per-app toggle. Absent body.app means the backend either
|
|
177
|
+
// doesn't know about appKey-scoped sync (older API) or no appKey was
|
|
178
|
+
// sent — leave the previous value untouched (default true on first run).
|
|
179
|
+
if (body.app && typeof body.app.firewallEnabled === 'boolean') {
|
|
180
|
+
const next = body.app.firewallEnabled;
|
|
181
|
+
if (next !== _lastRemoteEnabled) {
|
|
182
|
+
_remoteEnabled = next;
|
|
183
|
+
if (_options.log) {
|
|
184
|
+
console.log('[securenow] Firewall: remote toggle → %s (app=%s)',
|
|
185
|
+
next ? 'ENABLED' : 'DISABLED', body.app.key || _options.appKey);
|
|
186
|
+
}
|
|
187
|
+
_lastRemoteEnabled = next;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
168
191
|
// Update blocklist version + data
|
|
169
192
|
if (body.blocklist) {
|
|
170
193
|
const newVer = body.blocklist.version;
|
|
@@ -559,6 +582,14 @@ function sendBlockResponse(req, res, ip) {
|
|
|
559
582
|
}
|
|
560
583
|
|
|
561
584
|
function firewallRequestHandler(req, res) {
|
|
585
|
+
// Remote disable wins over everything: when the dashboard / CLI flips the
|
|
586
|
+
// toggle off, requests pass through the SDK as if the firewall weren't
|
|
587
|
+
// installed. The poll loop keeps running so we re-enable within seconds.
|
|
588
|
+
if (_remoteEnabled === false) {
|
|
589
|
+
_stats.suppressedDisabled++;
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
562
593
|
const ip = resolveClientIp(req);
|
|
563
594
|
|
|
564
595
|
// Allowlist check: if active, only listed IPs are allowed through
|
|
@@ -711,10 +742,16 @@ function getStats() {
|
|
|
711
742
|
circuitState: _circuitState,
|
|
712
743
|
consecutiveErrors: _consecutiveErrors,
|
|
713
744
|
unifiedSync: _useUnifiedSync,
|
|
745
|
+
remoteEnabled: _remoteEnabled,
|
|
746
|
+
appKey: _options ? _options.appKey || null : null,
|
|
714
747
|
};
|
|
715
748
|
}
|
|
716
749
|
|
|
717
|
-
|
|
718
|
-
|
|
750
|
+
// Layers (TCP / iptables / cloud) read the matcher to populate kernel-level
|
|
751
|
+
// rules. When the remote toggle is off, return null so they treat the policy
|
|
752
|
+
// as "no IPs to block" without us mutating the cached matcher.
|
|
753
|
+
function getMatcher() { return _remoteEnabled === false ? null : _matcher; }
|
|
754
|
+
function getAllowlistMatcher() { return _remoteEnabled === false ? null : _allowlistMatcher; }
|
|
755
|
+
function isRemoteEnabled() { return _remoteEnabled !== false; }
|
|
719
756
|
|
|
720
|
-
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher };
|
|
757
|
+
module.exports = { init, shutdown, getStats, getMatcher, getAllowlistMatcher, isRemoteEnabled };
|
package/nextjs.js
CHANGED
|
@@ -616,11 +616,14 @@ function registerSecureNow(options = {}) {
|
|
|
616
616
|
// Firewall — runs independently from OTel so it works even if tracing fails.
|
|
617
617
|
// Key comes from env OR .securenow/credentials.json (set via
|
|
618
618
|
// `npx securenow api-key set snk_live_...`), so you don't need a .env entry.
|
|
619
|
-
const
|
|
619
|
+
const appConfig = require('./app-config');
|
|
620
|
+
const firewallApiKey = appConfig.resolveApiKey();
|
|
621
|
+
const firewallAppKey = appConfig.resolveAppKey();
|
|
620
622
|
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
621
623
|
try {
|
|
622
624
|
require('./firewall').init({
|
|
623
625
|
apiKey: firewallApiKey,
|
|
626
|
+
appKey: firewallAppKey || null,
|
|
624
627
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
625
628
|
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
626
629
|
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
package/nuxt-server-plugin.mjs
CHANGED
|
@@ -328,11 +328,13 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
328
328
|
|
|
329
329
|
// ── Firewall — runs independently from OTel ──
|
|
330
330
|
const firewallApiKey = env('SECURENOW_API_KEY');
|
|
331
|
+
const firewallAppKey = env('SECURENOW_APPID') || null;
|
|
331
332
|
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
332
333
|
try {
|
|
333
334
|
const { init: fwInit } = await import('./firewall.js');
|
|
334
335
|
fwInit({
|
|
335
336
|
apiKey: firewallApiKey,
|
|
337
|
+
appKey: firewallAppKey,
|
|
336
338
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
337
339
|
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
338
340
|
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|
package/package.json
CHANGED
package/tracing.js
CHANGED
|
@@ -611,11 +611,15 @@ const sdk = new NodeSDK({
|
|
|
611
611
|
patchHttpForBanner();
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
// Firewall — auto-activates when
|
|
615
|
-
|
|
614
|
+
// Firewall — auto-activates only when a real snk_live_ key is resolvable.
|
|
615
|
+
// resolveApiKey() enforces the prefix, so we skip cleanly when the app has
|
|
616
|
+
// only an app-routing UUID (or nothing at all) — no 401 polling loops.
|
|
617
|
+
const firewallApiKey = appConfig.resolveApiKey();
|
|
618
|
+
const firewallAppKey = appConfig.resolveAppKey();
|
|
616
619
|
if (firewallApiKey && env('SECURENOW_FIREWALL_ENABLED') !== '0') {
|
|
617
620
|
require('./firewall').init({
|
|
618
621
|
apiKey: firewallApiKey,
|
|
622
|
+
appKey: firewallAppKey || null,
|
|
619
623
|
apiUrl: env('SECURENOW_API_URL') || 'https://api.securenow.ai',
|
|
620
624
|
versionCheckInterval: parseInt(env('SECURENOW_FIREWALL_VERSION_INTERVAL'), 10) || 10,
|
|
621
625
|
syncInterval: parseInt(env('SECURENOW_FIREWALL_SYNC_INTERVAL'), 10) || 300,
|