securenow 6.0.2 → 6.1.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.
Files changed (87) hide show
  1. package/CONSUMING-APPS-GUIDE.md +455 -0
  2. package/NPM_README.md +2029 -0
  3. package/README.md +297 -40
  4. package/SKILL-API.md +634 -0
  5. package/SKILL-CLI.md +454 -0
  6. package/cidr.js +83 -0
  7. package/cli/apps.js +585 -0
  8. package/cli/auth.js +280 -0
  9. package/cli/client.js +115 -0
  10. package/cli/config.js +173 -0
  11. package/cli/diagnostics.js +387 -0
  12. package/cli/firewall.js +100 -0
  13. package/cli/fp.js +638 -0
  14. package/cli/init.js +201 -0
  15. package/cli/monitor.js +440 -0
  16. package/cli/run.js +148 -0
  17. package/cli/security.js +980 -0
  18. package/cli/ui.js +386 -0
  19. package/cli/utils.js +127 -0
  20. package/cli.js +466 -455
  21. package/console-instrumentation.js +147 -136
  22. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +1377 -455
  23. package/docs/API-KEYS-GUIDE.md +233 -0
  24. package/docs/ARCHITECTURE.md +3 -3
  25. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  26. package/docs/AUTO-SETUP-SUMMARY.md +331 -0
  27. package/docs/AUTO-SETUP.md +4 -4
  28. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  29. package/docs/BODY-CAPTURE-FIX.md +261 -0
  30. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  31. package/docs/CHANGELOG-NEXTJS.md +1 -35
  32. package/docs/COMPLETION-REPORT.md +408 -0
  33. package/docs/CUSTOMER-GUIDE.md +16 -16
  34. package/docs/EASIEST-SETUP.md +5 -5
  35. package/docs/ENVIRONMENT-VARIABLES.md +880 -652
  36. package/docs/EXPRESS-BODY-CAPTURE.md +13 -12
  37. package/docs/EXPRESS-SETUP-GUIDE.md +719 -720
  38. package/docs/FINAL-SOLUTION.md +335 -0
  39. package/docs/FIREWALL-GUIDE.md +426 -0
  40. package/docs/IMPLEMENTATION-SUMMARY.md +410 -0
  41. package/docs/INDEX.md +22 -4
  42. package/docs/LOGGING-GUIDE.md +701 -708
  43. package/docs/LOGGING-QUICKSTART.md +234 -255
  44. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +323 -0
  45. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  46. package/docs/NEXTJS-GUIDE.md +14 -14
  47. package/docs/NEXTJS-QUICKSTART.md +1 -1
  48. package/docs/NEXTJS-SETUP-COMPLETE.md +795 -0
  49. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  50. package/docs/NUXT-GUIDE.md +166 -0
  51. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  52. package/docs/REDACTION-EXAMPLES.md +1 -1
  53. package/docs/REQUEST-BODY-CAPTURE.md +19 -10
  54. package/docs/SOLUTION-SUMMARY.md +312 -0
  55. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  56. package/examples/README.md +6 -6
  57. package/examples/instrumentation-with-auto-capture.ts +1 -1
  58. package/examples/nextjs-env-example.txt +2 -2
  59. package/examples/nextjs-instrumentation.js +1 -1
  60. package/examples/nextjs-instrumentation.ts +1 -1
  61. package/examples/nextjs-with-logging-example.md +6 -6
  62. package/examples/nextjs-with-options.ts +1 -1
  63. package/examples/test-nextjs-setup.js +1 -1
  64. package/firewall-cloud.js +212 -0
  65. package/firewall-iptables.js +139 -0
  66. package/firewall-only.js +38 -0
  67. package/firewall-tcp.js +74 -0
  68. package/firewall.js +720 -0
  69. package/free-trial-banner.js +174 -0
  70. package/nextjs-auto-capture.js +199 -207
  71. package/nextjs-middleware.js +186 -181
  72. package/nextjs-webpack-config.js +88 -53
  73. package/nextjs-wrapper.js +158 -158
  74. package/nextjs.d.ts +1 -1
  75. package/nextjs.js +639 -647
  76. package/nuxt-server-plugin.mjs +423 -0
  77. package/nuxt.d.ts +60 -0
  78. package/nuxt.mjs +75 -0
  79. package/package.json +186 -164
  80. package/postinstall.js +6 -6
  81. package/register.d.ts +1 -1
  82. package/register.js +39 -4
  83. package/resolve-ip.js +77 -0
  84. package/tracing.d.ts +2 -1
  85. package/tracing.js +295 -34
  86. package/web-vite.mjs +239 -156
  87. package/LICENSE +0 -15
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const url = require('url');
5
+ const ui = require('./ui');
6
+ const config = require('./config');
7
+
8
+ // ── Config resolution (mirrors tracing.js priority order) ──
9
+
10
+ function resolvedConfig() {
11
+ const serviceName =
12
+ process.env.OTEL_SERVICE_NAME ||
13
+ process.env.SECURENOW_APPID ||
14
+ '(auto-generated)';
15
+ const instance =
16
+ process.env.SECURENOW_INSTANCE || 'https://freetrial.securenow.ai:4318';
17
+ const tracesEndpoint =
18
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
19
+ (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
20
+ ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/traces`
21
+ : `${instance.replace(/\/$/, '')}/v1/traces`);
22
+ const logsEndpoint =
23
+ process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ||
24
+ (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
25
+ ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/$/, '')}/v1/logs`
26
+ : `${instance.replace(/\/$/, '')}/v1/logs`);
27
+ const headers = process.env.OTEL_EXPORTER_OTLP_HEADERS || '';
28
+ const apiKey = process.env.SECURENOW_API_KEY || '';
29
+ const apiUrl = config.getApiUrl();
30
+ const loggingEnabled = process.env.SECURENOW_LOGGING_ENABLED !== '0';
31
+ const captureBody = process.env.SECURENOW_CAPTURE_BODY === '1';
32
+ const firewallEnabled =
33
+ !!apiKey && process.env.SECURENOW_FIREWALL_ENABLED !== '0';
34
+
35
+ return {
36
+ serviceName,
37
+ instance,
38
+ tracesEndpoint,
39
+ logsEndpoint,
40
+ headers,
41
+ apiKey,
42
+ apiUrl,
43
+ loggingEnabled,
44
+ captureBody,
45
+ firewallEnabled,
46
+ firewallLayers: {
47
+ http: firewallEnabled,
48
+ tcp: firewallEnabled && process.env.SECURENOW_FIREWALL_TCP === '1',
49
+ iptables: firewallEnabled && process.env.SECURENOW_FIREWALL_IPTABLES === '1',
50
+ cloud: firewallEnabled ? process.env.SECURENOW_FIREWALL_CLOUD || null : null,
51
+ },
52
+ };
53
+ }
54
+
55
+ function parseHeaders(str) {
56
+ const out = {};
57
+ if (!str) return out;
58
+ for (const pair of str.split(',')) {
59
+ const [k, ...rest] = pair.split('=');
60
+ if (!k) continue;
61
+ out[k.trim()] = rest.join('=').trim();
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // ── HTTP helpers ──
67
+
68
+ function httpRequest({ method = 'POST', endpoint, headers = {}, body, timeoutMs = 5000 }) {
69
+ return new Promise((resolve, reject) => {
70
+ const parsed = new url.URL(endpoint);
71
+ const lib = parsed.protocol === 'https:' ? require('https') : require('http');
72
+ const payload = body == null ? null : Buffer.from(body);
73
+ const req = lib.request({
74
+ method,
75
+ hostname: parsed.hostname,
76
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
77
+ path: parsed.pathname + parsed.search,
78
+ headers: {
79
+ ...(payload ? { 'Content-Length': payload.length } : {}),
80
+ ...headers,
81
+ },
82
+ timeout: timeoutMs,
83
+ }, (res) => {
84
+ const chunks = [];
85
+ res.on('data', (c) => chunks.push(c));
86
+ res.on('end', () => {
87
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') });
88
+ });
89
+ });
90
+ req.on('error', reject);
91
+ req.on('timeout', () => { req.destroy(new Error(`timeout after ${timeoutMs}ms`)); });
92
+ if (payload) req.write(payload);
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ // ── OTLP/HTTP JSON payloads ──
98
+
99
+ function attr(key, value) {
100
+ if (typeof value === 'number') {
101
+ if (Number.isInteger(value)) return { key, value: { intValue: String(value) } };
102
+ return { key, value: { doubleValue: value } };
103
+ }
104
+ if (typeof value === 'boolean') return { key, value: { boolValue: value } };
105
+ return { key, value: { stringValue: String(value) } };
106
+ }
107
+
108
+ function resourceAttrs(cfg, extra = {}) {
109
+ const base = {
110
+ 'service.name': cfg.serviceName,
111
+ 'deployment.environment': process.env.NODE_ENV || 'development',
112
+ 'telemetry.sdk.name': 'securenow-cli',
113
+ 'telemetry.sdk.language': 'nodejs',
114
+ ...extra,
115
+ };
116
+ return Object.entries(base).map(([k, v]) => attr(k, v));
117
+ }
118
+
119
+ function randomHex(bytes) {
120
+ return crypto.randomBytes(bytes).toString('hex');
121
+ }
122
+
123
+ function buildTracePayload(cfg, { name, attributes = {} }) {
124
+ const now = BigInt(Date.now()) * 1000000n;
125
+ const end = now + 1000000n;
126
+ return {
127
+ resourceSpans: [{
128
+ resource: { attributes: resourceAttrs(cfg) },
129
+ scopeSpans: [{
130
+ scope: { name: 'securenow-cli', version: '1.0.0' },
131
+ spans: [{
132
+ traceId: randomHex(16),
133
+ spanId: randomHex(8),
134
+ name,
135
+ kind: 1,
136
+ startTimeUnixNano: now.toString(),
137
+ endTimeUnixNano: end.toString(),
138
+ attributes: Object.entries(attributes).map(([k, v]) => attr(k, v)),
139
+ status: { code: 1 },
140
+ }],
141
+ }],
142
+ }],
143
+ };
144
+ }
145
+
146
+ const SEVERITY_MAP = {
147
+ trace: { number: 1, text: 'TRACE' },
148
+ debug: { number: 5, text: 'DEBUG' },
149
+ info: { number: 9, text: 'INFO' },
150
+ warn: { number: 13, text: 'WARN' },
151
+ warning: { number: 13, text: 'WARN' },
152
+ error: { number: 17, text: 'ERROR' },
153
+ fatal: { number: 21, text: 'FATAL' },
154
+ };
155
+
156
+ function buildLogPayload(cfg, { message, level = 'info', attributes = {} }) {
157
+ const now = BigInt(Date.now()) * 1000000n;
158
+ const sev = SEVERITY_MAP[level.toLowerCase()] || SEVERITY_MAP.info;
159
+ return {
160
+ resourceLogs: [{
161
+ resource: { attributes: resourceAttrs(cfg) },
162
+ scopeLogs: [{
163
+ scope: { name: 'securenow-cli', version: '1.0.0' },
164
+ logRecords: [{
165
+ timeUnixNano: now.toString(),
166
+ observedTimeUnixNano: now.toString(),
167
+ severityNumber: sev.number,
168
+ severityText: sev.text,
169
+ body: { stringValue: message },
170
+ attributes: Object.entries(attributes).map(([k, v]) => attr(k, v)),
171
+ }],
172
+ }],
173
+ }],
174
+ };
175
+ }
176
+
177
+ // ── Commands ──
178
+
179
+ async function testSpan(args, flags) {
180
+ const cfg = resolvedConfig();
181
+ const spanName = args[0] || 'securenow.cli.test-span';
182
+ const headers = {
183
+ 'Content-Type': 'application/json',
184
+ ...parseHeaders(cfg.headers),
185
+ };
186
+ const payload = buildTracePayload(cfg, {
187
+ name: spanName,
188
+ attributes: { 'test.source': 'securenow-cli' },
189
+ });
190
+
191
+ const spin = ui.spinner(`Sending test span to ${cfg.tracesEndpoint}`);
192
+ try {
193
+ const res = await httpRequest({
194
+ endpoint: cfg.tracesEndpoint,
195
+ headers,
196
+ body: JSON.stringify(payload),
197
+ });
198
+ if (res.status >= 200 && res.status < 300) {
199
+ spin.stop(`Span accepted (HTTP ${res.status})`);
200
+ if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.tracesEndpoint });
201
+ return;
202
+ }
203
+ spin.fail(`Collector returned HTTP ${res.status}`);
204
+ if (res.body) console.log(ui.c.dim(res.body.slice(0, 500)));
205
+ if (flags.json) ui.json({ ok: false, status: res.status, body: res.body });
206
+ process.exit(1);
207
+ } catch (err) {
208
+ spin.fail(`Failed: ${err.message}`);
209
+ if (flags.json) ui.json({ ok: false, error: err.message });
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ async function logSend(args, flags) {
215
+ const message = args.join(' ').trim();
216
+ if (!message) {
217
+ ui.error('Missing log message.');
218
+ console.log(` ${ui.c.bold('Usage:')} securenow log send "<message>" [--level info|warn|error] [--attrs k=v,k=v]`);
219
+ process.exit(1);
220
+ }
221
+
222
+ const cfg = resolvedConfig();
223
+ if (!cfg.loggingEnabled) {
224
+ ui.warn('Logging is disabled (SECURENOW_LOGGING_ENABLED=0). Sending anyway.');
225
+ }
226
+
227
+ const level = (flags.level || 'info').toString();
228
+ const attributes = {};
229
+ if (flags.attrs) {
230
+ for (const pair of String(flags.attrs).split(',')) {
231
+ const [k, ...rest] = pair.split('=');
232
+ if (k && rest.length) attributes[k.trim()] = rest.join('=').trim();
233
+ }
234
+ }
235
+
236
+ const headers = {
237
+ 'Content-Type': 'application/json',
238
+ ...parseHeaders(cfg.headers),
239
+ };
240
+ const payload = buildLogPayload(cfg, { message, level, attributes });
241
+
242
+ const spin = ui.spinner(`Sending log to ${cfg.logsEndpoint}`);
243
+ try {
244
+ const res = await httpRequest({
245
+ endpoint: cfg.logsEndpoint,
246
+ headers,
247
+ body: JSON.stringify(payload),
248
+ });
249
+ if (res.status >= 200 && res.status < 300) {
250
+ spin.stop(`Log accepted (HTTP ${res.status})`);
251
+ if (flags.json) ui.json({ ok: true, status: res.status, endpoint: cfg.logsEndpoint });
252
+ return;
253
+ }
254
+ spin.fail(`Collector returned HTTP ${res.status}`);
255
+ if (res.body) console.log(ui.c.dim(res.body.slice(0, 500)));
256
+ if (flags.json) ui.json({ ok: false, status: res.status, body: res.body });
257
+ process.exit(1);
258
+ } catch (err) {
259
+ spin.fail(`Failed: ${err.message}`);
260
+ if (flags.json) ui.json({ ok: false, error: err.message });
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ // ── doctor / env ──
266
+
267
+ async function probe(endpoint, timeoutMs = 3000) {
268
+ try {
269
+ // A HEAD or empty POST is safer than sending real OTLP. Most collectors
270
+ // return 400/405 for a malformed request — that still proves reachability.
271
+ const res = await httpRequest({
272
+ method: 'POST',
273
+ endpoint,
274
+ headers: { 'Content-Type': 'application/json' },
275
+ body: '{}',
276
+ timeoutMs,
277
+ });
278
+ return { ok: true, status: res.status };
279
+ } catch (err) {
280
+ return { ok: false, error: err.message };
281
+ }
282
+ }
283
+
284
+ function env(_args, flags) {
285
+ const cfg = resolvedConfig();
286
+ const vars = {
287
+ SECURENOW_APPID: process.env.SECURENOW_APPID || null,
288
+ OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME || null,
289
+ SECURENOW_INSTANCE: process.env.SECURENOW_INSTANCE || null,
290
+ OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || null,
291
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || null,
292
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || null,
293
+ OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS ? '***' : null,
294
+ SECURENOW_API_KEY: cfg.apiKey ? `${cfg.apiKey.slice(0, 12)}...` : null,
295
+ SECURENOW_API_URL: cfg.apiUrl,
296
+ SECURENOW_LOGGING_ENABLED: cfg.loggingEnabled ? '1' : '0',
297
+ SECURENOW_CAPTURE_BODY: cfg.captureBody ? '1' : '0',
298
+ SECURENOW_NO_UUID: process.env.SECURENOW_NO_UUID || '0',
299
+ SECURENOW_FIREWALL_TCP: process.env.SECURENOW_FIREWALL_TCP || '0',
300
+ SECURENOW_FIREWALL_IPTABLES: process.env.SECURENOW_FIREWALL_IPTABLES || '0',
301
+ SECURENOW_FIREWALL_CLOUD: process.env.SECURENOW_FIREWALL_CLOUD || null,
302
+ NODE_ENV: process.env.NODE_ENV || 'development',
303
+ };
304
+
305
+ if (flags.json) {
306
+ ui.json({ resolved: cfg, env: vars });
307
+ return;
308
+ }
309
+
310
+ ui.heading('Resolved configuration');
311
+ ui.keyValue([
312
+ ['Service name', cfg.serviceName],
313
+ ['Traces endpoint', cfg.tracesEndpoint],
314
+ ['Logs endpoint', cfg.logsEndpoint],
315
+ ['Logging', cfg.loggingEnabled ? ui.c.green('enabled') : ui.c.dim('disabled')],
316
+ ['Body capture', cfg.captureBody ? ui.c.green('enabled') : ui.c.dim('disabled')],
317
+ ['Firewall', cfg.firewallEnabled ? ui.c.green('enabled') : ui.c.dim('disabled (no API key)')],
318
+ ]);
319
+
320
+ ui.heading('Environment variables');
321
+ ui.keyValue(
322
+ Object.entries(vars).map(([k, v]) => [k, v == null ? ui.c.dim('(not set)') : String(v)])
323
+ );
324
+ console.log('');
325
+ }
326
+
327
+ async function doctor(_args, flags) {
328
+ const cfg = resolvedConfig();
329
+ const checks = [];
330
+
331
+ // Collector traces
332
+ const spin1 = ui.spinner(`Probing traces endpoint ${cfg.tracesEndpoint}`);
333
+ const traces = await probe(cfg.tracesEndpoint);
334
+ if (traces.ok) spin1.stop(`Traces endpoint reachable (HTTP ${traces.status})`);
335
+ else spin1.fail(`Traces endpoint unreachable: ${traces.error}`);
336
+ checks.push({ name: 'traces', ...traces });
337
+
338
+ // Collector logs
339
+ const spin2 = ui.spinner(`Probing logs endpoint ${cfg.logsEndpoint}`);
340
+ const logs = await probe(cfg.logsEndpoint);
341
+ if (logs.ok) spin2.stop(`Logs endpoint reachable (HTTP ${logs.status})`);
342
+ else spin2.fail(`Logs endpoint unreachable: ${logs.error}`);
343
+ checks.push({ name: 'logs', ...logs });
344
+
345
+ // SecureNow API (only if API key or logged-in token)
346
+ const token = config.getToken();
347
+ if (cfg.apiKey || token) {
348
+ const spin3 = ui.spinner(`Probing SecureNow API ${cfg.apiUrl}`);
349
+ const api = await probe(`${cfg.apiUrl.replace(/\/$/, '')}/health`);
350
+ if (api.ok) spin3.stop(`API reachable (HTTP ${api.status})`);
351
+ else spin3.fail(`API unreachable: ${api.error}`);
352
+ checks.push({ name: 'api', ...api });
353
+ }
354
+
355
+ // Config sanity
356
+ const warnings = [];
357
+ if (!process.env.SECURENOW_APPID && !process.env.OTEL_SERVICE_NAME) {
358
+ warnings.push('No SECURENOW_APPID or OTEL_SERVICE_NAME set — a UUID-suffixed name will be generated.');
359
+ }
360
+ if (cfg.instance === 'https://freetrial.securenow.ai:4318') {
361
+ warnings.push('Using the free-trial collector — set SECURENOW_INSTANCE for production.');
362
+ }
363
+ if (!cfg.apiKey && cfg.firewallEnabled) {
364
+ warnings.push('Firewall enabled but SECURENOW_API_KEY missing — blocklist will not sync.');
365
+ }
366
+
367
+ const ok = checks.every((c) => c.ok);
368
+
369
+ if (flags.json) {
370
+ ui.json({ ok, resolved: cfg, checks, warnings });
371
+ process.exit(ok ? 0 : 1);
372
+ }
373
+
374
+ if (warnings.length) {
375
+ ui.heading('Warnings');
376
+ for (const w of warnings) ui.warn(w);
377
+ }
378
+
379
+ console.log('');
380
+ if (ok) ui.success('All checks passed.');
381
+ else ui.error('One or more checks failed. Run with --json for details.');
382
+ console.log('');
383
+
384
+ process.exit(ok ? 0 : 1);
385
+ }
386
+
387
+ module.exports = { testSpan, logSend, doctor, env };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const { api, requireAuth } = require('./client');
4
+ const ui = require('./ui');
5
+
6
+ async function status(args, flags) {
7
+ requireAuth();
8
+ const s = ui.spinner('Checking firewall status');
9
+
10
+ try {
11
+ const data = await api.get('/firewall/status');
12
+
13
+ s.stop('Firewall status retrieved');
14
+
15
+ if (flags.json) {
16
+ ui.json(data);
17
+ return;
18
+ }
19
+
20
+ console.log('');
21
+ console.log(` ${ui.c.bold(ui.c.green('Firewall: ENABLED'))}`);
22
+ console.log('');
23
+ ui.keyValue([
24
+ ['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
25
+ ['Last updated', data.updatedAt || 'unknown'],
26
+ ['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
27
+ ['Allowlist updated', data.allowlistUpdatedAt || 'never'],
28
+ ['Sync TTL', `${data.ttl || 60}s`],
29
+ ]);
30
+ if (data.allowlistCount > 0) {
31
+ console.log('');
32
+ console.log(` ${ui.c.yellow('!')} Allowlist is active — only ${data.allowlistCount} IP(s) can reach your app`);
33
+ }
34
+ console.log('');
35
+ console.log(` ${ui.c.dim('Manage your blocklist:')} securenow blocklist`);
36
+ console.log(` ${ui.c.dim('Manage your allowlist:')} securenow allowlist`);
37
+ console.log(` ${ui.c.dim('Test an IP:')} securenow firewall test-ip <ip>`);
38
+ console.log('');
39
+ } catch (err) {
40
+ if (err.statusCode === 401 || err.status === 401) {
41
+ s.fail('Authentication failed');
42
+ ui.error('API key is invalid or missing the firewall:read scope.');
43
+ ui.info('Create an API key with firewall:read scope in the dashboard.');
44
+ } else {
45
+ s.fail('Failed to check firewall status');
46
+ throw err;
47
+ }
48
+ }
49
+ }
50
+
51
+ async function testIp(args, flags) {
52
+ requireAuth();
53
+ const ip = args[0];
54
+ if (!ip) {
55
+ ui.error('Usage: securenow firewall test-ip <ip-address>');
56
+ process.exit(1);
57
+ }
58
+
59
+ const s = ui.spinner(`Testing IP ${ip}`);
60
+
61
+ try {
62
+ const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`);
63
+
64
+ s.stop(`IP ${ip} checked`);
65
+
66
+ if (flags.json) {
67
+ ui.json(data);
68
+ return;
69
+ }
70
+
71
+ console.log('');
72
+ if (data.blocked) {
73
+ if (data.allowlistActive && !data.allowlisted) {
74
+ console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is not on the allowlist`);
75
+ console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
76
+ } else {
77
+ console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is in the blocklist`);
78
+ if (data.matchedEntry && data.matchedEntry !== ip) {
79
+ console.log(` ${ui.c.dim(`Matched by: ${data.matchedEntry}`)}`);
80
+ }
81
+ }
82
+ } else {
83
+ if (data.allowlisted) {
84
+ console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is on the allowlist`);
85
+ } else {
86
+ console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
87
+ }
88
+ }
89
+ console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
90
+ if (data.allowlistActive) {
91
+ console.log(` ${ui.c.dim('Allowlist is active')}`);
92
+ }
93
+ console.log('');
94
+ } catch (err) {
95
+ s.fail('Failed to test IP');
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ module.exports = { status, testIp };