securenow 5.18.0 → 6.0.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 (85) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +40 -239
  3. package/cli.js +455 -415
  4. package/console-instrumentation.js +136 -147
  5. package/docs/ALL-FRAMEWORKS-QUICKSTART.md +455 -1339
  6. package/docs/ARCHITECTURE.md +3 -3
  7. package/docs/AUTO-BODY-CAPTURE.md +1 -1
  8. package/docs/AUTO-SETUP.md +4 -4
  9. package/docs/AUTOMATIC-IP-CAPTURE.md +5 -5
  10. package/docs/BODY-CAPTURE-QUICKSTART.md +2 -2
  11. package/docs/CHANGELOG-NEXTJS.md +1 -1
  12. package/docs/CUSTOMER-GUIDE.md +16 -16
  13. package/docs/EASIEST-SETUP.md +5 -5
  14. package/docs/ENVIRONMENT-VARIABLES.md +652 -880
  15. package/docs/EXPRESS-BODY-CAPTURE.md +12 -13
  16. package/docs/EXPRESS-SETUP-GUIDE.md +720 -719
  17. package/docs/INDEX.md +4 -22
  18. package/docs/LOGGING-GUIDE.md +708 -701
  19. package/docs/LOGGING-QUICKSTART.md +239 -234
  20. package/docs/NEXTJS-BODY-CAPTURE.md +2 -2
  21. package/docs/NEXTJS-GUIDE.md +14 -14
  22. package/docs/NEXTJS-QUICKSTART.md +1 -1
  23. package/docs/NEXTJS-WRAPPER-APPROACH.md +1 -1
  24. package/docs/QUICKSTART-BODY-CAPTURE.md +2 -2
  25. package/docs/REDACTION-EXAMPLES.md +1 -1
  26. package/docs/REQUEST-BODY-CAPTURE.md +10 -19
  27. package/docs/VERCEL-OTEL-MIGRATION.md +3 -3
  28. package/examples/README.md +6 -6
  29. package/examples/instrumentation-with-auto-capture.ts +1 -1
  30. package/examples/nextjs-env-example.txt +2 -2
  31. package/examples/nextjs-instrumentation.js +1 -1
  32. package/examples/nextjs-instrumentation.ts +1 -1
  33. package/examples/nextjs-with-logging-example.md +6 -6
  34. package/examples/nextjs-with-options.ts +1 -1
  35. package/examples/test-nextjs-setup.js +1 -1
  36. package/nextjs-auto-capture.js +207 -199
  37. package/nextjs-middleware.js +181 -186
  38. package/nextjs-webpack-config.js +53 -88
  39. package/nextjs-wrapper.js +158 -158
  40. package/nextjs.d.ts +1 -1
  41. package/nextjs.js +135 -190
  42. package/package.json +45 -67
  43. package/postinstall.js +6 -6
  44. package/register.d.ts +1 -1
  45. package/register.js +4 -39
  46. package/tracing.d.ts +1 -2
  47. package/tracing.js +22 -287
  48. package/web-vite.mjs +156 -239
  49. package/CONSUMING-APPS-GUIDE.md +0 -455
  50. package/NPM_README.md +0 -1933
  51. package/SKILL-API.md +0 -600
  52. package/SKILL-CLI.md +0 -409
  53. package/cidr.js +0 -83
  54. package/cli/apps.js +0 -585
  55. package/cli/auth.js +0 -280
  56. package/cli/client.js +0 -115
  57. package/cli/config.js +0 -173
  58. package/cli/firewall.js +0 -100
  59. package/cli/fp.js +0 -638
  60. package/cli/init.js +0 -201
  61. package/cli/monitor.js +0 -440
  62. package/cli/run.js +0 -133
  63. package/cli/security.js +0 -1064
  64. package/cli/ui.js +0 -386
  65. package/docs/API-KEYS-GUIDE.md +0 -233
  66. package/docs/AUTO-SETUP-SUMMARY.md +0 -331
  67. package/docs/BODY-CAPTURE-FIX.md +0 -261
  68. package/docs/COMPLETION-REPORT.md +0 -408
  69. package/docs/FINAL-SOLUTION.md +0 -335
  70. package/docs/FIREWALL-GUIDE.md +0 -426
  71. package/docs/IMPLEMENTATION-SUMMARY.md +0 -410
  72. package/docs/NEXTJS-BODY-CAPTURE-COMPARISON.md +0 -323
  73. package/docs/NEXTJS-SETUP-COMPLETE.md +0 -795
  74. package/docs/NUXT-GUIDE.md +0 -166
  75. package/docs/SOLUTION-SUMMARY.md +0 -312
  76. package/firewall-cloud.js +0 -212
  77. package/firewall-iptables.js +0 -139
  78. package/firewall-only.js +0 -38
  79. package/firewall-tcp.js +0 -74
  80. package/firewall.js +0 -720
  81. package/free-trial-banner.js +0 -174
  82. package/nuxt-server-plugin.mjs +0 -423
  83. package/nuxt.d.ts +0 -60
  84. package/nuxt.mjs +0 -75
  85. package/resolve-ip.js +0 -77
package/cli/ui.js DELETED
@@ -1,386 +0,0 @@
1
- 'use strict';
2
-
3
- const readline = require('readline');
4
-
5
- const NO_COLOR = !!process.env.NO_COLOR || !process.stdout.isTTY;
6
-
7
- const codes = {
8
- reset: '\x1b[0m',
9
- bold: '\x1b[1m',
10
- dim: '\x1b[2m',
11
- italic: '\x1b[3m',
12
- underline: '\x1b[4m',
13
- red: '\x1b[31m',
14
- green: '\x1b[32m',
15
- yellow: '\x1b[33m',
16
- blue: '\x1b[34m',
17
- magenta: '\x1b[35m',
18
- cyan: '\x1b[36m',
19
- white: '\x1b[37m',
20
- gray: '\x1b[90m',
21
- bgRed: '\x1b[41m',
22
- bgGreen: '\x1b[42m',
23
- bgYellow: '\x1b[43m',
24
- bgBlue: '\x1b[44m',
25
- bgCyan: '\x1b[46m',
26
- };
27
-
28
- function style(code, text) {
29
- if (NO_COLOR) return text;
30
- return `${code}${text}${codes.reset}`;
31
- }
32
-
33
- const c = {
34
- bold: (s) => style(codes.bold, s),
35
- dim: (s) => style(codes.dim, s),
36
- italic: (s) => style(codes.italic, s),
37
- underline: (s) => style(codes.underline, s),
38
- red: (s) => style(codes.red, s),
39
- green: (s) => style(codes.green, s),
40
- yellow: (s) => style(codes.yellow, s),
41
- blue: (s) => style(codes.blue, s),
42
- magenta: (s) => style(codes.magenta, s),
43
- cyan: (s) => style(codes.cyan, s),
44
- white: (s) => style(codes.white, s),
45
- gray: (s) => style(codes.gray, s),
46
- success: (s) => style(codes.green, s),
47
- error: (s) => style(codes.red, s),
48
- warn: (s) => style(codes.yellow, s),
49
- info: (s) => style(codes.cyan, s),
50
- };
51
-
52
- function stripAnsi(str) {
53
- return str.replace(/\x1b\[[0-9;]*m/g, '');
54
- }
55
-
56
- function visibleLength(str) {
57
- return stripAnsi(str).length;
58
- }
59
-
60
- function padEnd(str, len) {
61
- const visible = visibleLength(str);
62
- if (visible >= len) return str;
63
- return str + ' '.repeat(len - visible);
64
- }
65
-
66
- function table(headers, rows, opts = {}) {
67
- if (!rows.length) {
68
- console.log(c.dim(' No results found.'));
69
- return;
70
- }
71
-
72
- const maxWidth = process.stdout.columns || 120;
73
- const colWidths = headers.map((h, i) => {
74
- const headerLen = visibleLength(String(h));
75
- const maxCell = rows.reduce((max, row) => {
76
- const cell = String(row[i] ?? '');
77
- return Math.max(max, visibleLength(cell));
78
- }, 0);
79
- return Math.min(Math.max(headerLen, maxCell), opts.maxColWidth || 60);
80
- });
81
-
82
- const totalWidth = colWidths.reduce((a, b) => a + b, 0) + (colWidths.length - 1) * 3;
83
- if (totalWidth > maxWidth && colWidths.length > 1) {
84
- const lastIdx = colWidths.length - 1;
85
- const excess = totalWidth - maxWidth;
86
- colWidths[lastIdx] = Math.max(10, colWidths[lastIdx] - excess);
87
- }
88
-
89
- const headerLine = headers.map((h, i) => padEnd(c.bold(String(h)), colWidths[i])).join(' ');
90
- const separator = colWidths.map(w => c.dim('─'.repeat(w))).join('───');
91
-
92
- console.log(` ${headerLine}`);
93
- console.log(` ${separator}`);
94
-
95
- for (const row of rows) {
96
- const line = row.map((cell, i) => {
97
- let s = String(cell ?? '');
98
- if (visibleLength(s) > colWidths[i]) {
99
- s = stripAnsi(s).slice(0, colWidths[i] - 1) + '…';
100
- }
101
- return padEnd(s, colWidths[i]);
102
- }).join(' ');
103
- console.log(` ${line}`);
104
- }
105
- }
106
-
107
- function keyValue(pairs) {
108
- const maxKey = pairs.reduce((max, [k]) => Math.max(max, k.length), 0);
109
- for (const [key, value] of pairs) {
110
- console.log(` ${c.bold(key.padEnd(maxKey))} ${value ?? c.dim('—')}`);
111
- }
112
- }
113
-
114
- function heading(text) {
115
- console.log(`\n${c.bold(c.cyan(text))}`);
116
- }
117
-
118
- function subheading(text) {
119
- console.log(`\n ${c.bold(text)}`);
120
- }
121
-
122
- function success(msg) {
123
- console.log(`${c.green('✓')} ${msg}`);
124
- }
125
-
126
- function error(msg) {
127
- console.error(`${c.red('✗')} ${msg}`);
128
- }
129
-
130
- function warn(msg) {
131
- console.log(`${c.yellow('!')} ${msg}`);
132
- }
133
-
134
- function info(msg) {
135
- console.log(`${c.cyan('ℹ')} ${msg}`);
136
- }
137
-
138
- function spinner(message) {
139
- if (!process.stderr.isTTY) {
140
- process.stderr.write(` ${message}...\n`);
141
- return {
142
- update() {},
143
- stop(msg) { if (msg) process.stderr.write(` ${msg}\n`); },
144
- fail(msg) { if (msg) process.stderr.write(` ${msg}\n`); },
145
- };
146
- }
147
-
148
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
149
- let i = 0;
150
- let currentMsg = message;
151
- const interval = setInterval(() => {
152
- const frame = c.cyan(frames[i++ % frames.length]);
153
- process.stderr.write(`\r ${frame} ${currentMsg}`);
154
- }, 80);
155
-
156
- return {
157
- update(msg) { currentMsg = msg; },
158
- stop(finalMsg) {
159
- clearInterval(interval);
160
- process.stderr.write(`\r ${c.green('✓')} ${finalMsg || currentMsg}\n`);
161
- },
162
- fail(finalMsg) {
163
- clearInterval(interval);
164
- process.stderr.write(`\r ${c.red('✗')} ${finalMsg || currentMsg}\n`);
165
- },
166
- };
167
- }
168
-
169
- function prompt(question, opts = {}) {
170
- const rl = readline.createInterface({
171
- input: process.stdin,
172
- output: process.stderr,
173
- });
174
-
175
- return new Promise((resolve) => {
176
- const q = opts.secret ? question : ` ${c.cyan('?')} ${question} `;
177
- rl.question(q, (answer) => {
178
- rl.close();
179
- resolve(answer.trim());
180
- });
181
-
182
- if (opts.secret) {
183
- const onData = (char) => {
184
- const code = char.toString();
185
- if (code === '\n' || code === '\r' || code === '\u0004') return;
186
- process.stderr.write('*');
187
- };
188
- process.stdin.on('data', onData);
189
- rl.once('close', () => process.stdin.removeListener('data', onData));
190
- }
191
- });
192
- }
193
-
194
- async function confirm(question, defaultYes = false) {
195
- const hint = defaultYes ? 'Y/n' : 'y/N';
196
- const answer = await prompt(`${question} ${c.dim(`(${hint})`)}`);
197
- if (!answer) return defaultYes;
198
- return answer.toLowerCase().startsWith('y');
199
- }
200
-
201
- async function select(question, choices) {
202
- console.log(`\n ${c.cyan('?')} ${question}\n`);
203
- choices.forEach((choice, i) => {
204
- console.log(` ${c.bold(String(i + 1))} ${choice.label || choice}`);
205
- });
206
- console.log('');
207
- const answer = await prompt('Enter number');
208
- const idx = parseInt(answer, 10) - 1;
209
- if (idx < 0 || idx >= choices.length) {
210
- error('Invalid selection');
211
- process.exit(1);
212
- }
213
- return choices[idx].value ?? choices[idx];
214
- }
215
-
216
- async function multiSelect(question, choices) {
217
- console.log(`\n ${c.cyan('?')} ${question}\n`);
218
- choices.forEach((choice, i) => {
219
- const label = choice.label || choice;
220
- const detail = choice.detail ? c.dim(` — ${choice.detail}`) : '';
221
- console.log(` ${c.bold(String(i + 1).padStart(3))} ${label}${detail}`);
222
- });
223
- console.log('');
224
- console.log(c.dim(` Enter numbers separated by commas, a range (e.g. 1-5), or "all"`));
225
- const answer = await prompt('Selection');
226
-
227
- if (!answer) return [];
228
-
229
- if (answer.toLowerCase() === 'all') {
230
- return choices.map((ch) => ch.value ?? ch);
231
- }
232
-
233
- const indices = new Set();
234
- for (const part of answer.split(',')) {
235
- const trimmed = part.trim();
236
- if (trimmed.includes('-')) {
237
- const [startStr, endStr] = trimmed.split('-');
238
- const start = parseInt(startStr, 10);
239
- const end = parseInt(endStr, 10);
240
- if (!isNaN(start) && !isNaN(end)) {
241
- for (let i = Math.min(start, end); i <= Math.max(start, end); i++) {
242
- indices.add(i);
243
- }
244
- }
245
- } else {
246
- const num = parseInt(trimmed, 10);
247
- if (!isNaN(num)) indices.add(num);
248
- }
249
- }
250
-
251
- const selected = [];
252
- for (const idx of indices) {
253
- if (idx >= 1 && idx <= choices.length) {
254
- selected.push(choices[idx - 1].value ?? choices[idx - 1]);
255
- }
256
- }
257
- return selected;
258
- }
259
-
260
- function json(data) {
261
- console.log(JSON.stringify(data, null, 2));
262
- }
263
-
264
- function hr() {
265
- const width = Math.min(process.stdout.columns || 80, 80);
266
- console.log(c.dim('─'.repeat(width)));
267
- }
268
-
269
- // Parses timestamps the backend returns in multiple formats:
270
- // - UInt64 nanoseconds as a string (signoz logs) → "1775856777891000000"
271
- // - ClickHouse DateTime64 string (signoz traces) → "2026-04-10 21:34:51.133000000" (UTC, no Z)
272
- // - ISO 8601 → "2026-04-10T21:34:51.133Z"
273
- // - number (ms) / Date → passthrough
274
- function parseTimestamp(ts) {
275
- if (ts == null || ts === '') return null;
276
- if (ts instanceof Date) return isNaN(ts.getTime()) ? null : ts;
277
- if (typeof ts === 'number') {
278
- const d = new Date(ts);
279
- return isNaN(d.getTime()) ? null : d;
280
- }
281
- const s = String(ts).trim();
282
- if (/^\d{16,}$/.test(s)) {
283
- const d = new Date(Number(s) / 1e6);
284
- return isNaN(d.getTime()) ? null : d;
285
- }
286
- if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(s) && !/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
287
- const isoish = s.replace(' ', 'T').replace(/\.(\d{3})\d*$/, '.$1') + 'Z';
288
- const d = new Date(isoish);
289
- if (!isNaN(d.getTime())) return d;
290
- }
291
- const d = new Date(s);
292
- return isNaN(d.getTime()) ? null : d;
293
- }
294
-
295
- function timeAgo(dateStr) {
296
- const date = parseTimestamp(dateStr);
297
- if (!date) return '—';
298
- const diff = Date.now() - date.getTime();
299
- const seconds = Math.floor(diff / 1000);
300
- if (seconds < 60) return 'just now';
301
- const minutes = Math.floor(seconds / 60);
302
- if (minutes < 60) return `${minutes}m ago`;
303
- const hours = Math.floor(minutes / 60);
304
- if (hours < 24) return `${hours}h ago`;
305
- const days = Math.floor(hours / 24);
306
- if (days < 30) return `${days}d ago`;
307
- return date.toLocaleDateString();
308
- }
309
-
310
- function truncate(str, len = 50) {
311
- if (!str) return '';
312
- str = String(str);
313
- if (str.length <= len) return str;
314
- return str.slice(0, len - 1) + '…';
315
- }
316
-
317
- function statusBadge(status) {
318
- const map = {
319
- active: c.green('● active'),
320
- inactive: c.dim('○ inactive'),
321
- healthy: c.green('● healthy'),
322
- unhealthy: c.red('● unhealthy'),
323
- warning: c.yellow('● warning'),
324
- open: c.red('● open'),
325
- resolved: c.green('● resolved'),
326
- closed: c.dim('● closed'),
327
- acknowledged: c.yellow('● ack'),
328
- blocked: c.red('■ blocked'),
329
- trusted: c.green('■ trusted'),
330
- enabled: c.green('● enabled'),
331
- disabled: c.dim('○ disabled'),
332
- paused: c.yellow('◆ paused'),
333
- critical: c.red('▲ critical'),
334
- high: c.red('▲ high'),
335
- medium: c.yellow('▲ medium'),
336
- low: c.blue('▲ low'),
337
- info: c.cyan('▲ info'),
338
- read: c.dim('○ read'),
339
- unread: c.cyan('● unread'),
340
- };
341
- return map[(status || '').toLowerCase()] || status;
342
- }
343
-
344
- function httpStatusColor(code) {
345
- code = parseInt(code, 10);
346
- if (code < 300) return c.green(code);
347
- if (code < 400) return c.cyan(code);
348
- if (code < 500) return c.yellow(code);
349
- return c.red(code);
350
- }
351
-
352
- function durationColor(ms) {
353
- ms = parseFloat(ms);
354
- if (isNaN(ms)) return '—';
355
- if (ms < 100) return c.green(`${ms.toFixed(0)}ms`);
356
- if (ms < 500) return c.yellow(`${ms.toFixed(0)}ms`);
357
- if (ms < 1000) return c.red(`${ms.toFixed(0)}ms`);
358
- return c.red(`${(ms / 1000).toFixed(2)}s`);
359
- }
360
-
361
- module.exports = {
362
- c,
363
- NO_COLOR,
364
- stripAnsi,
365
- table,
366
- keyValue,
367
- heading,
368
- subheading,
369
- success,
370
- error,
371
- warn,
372
- info,
373
- spinner,
374
- prompt,
375
- confirm,
376
- select,
377
- multiSelect,
378
- json,
379
- hr,
380
- timeAgo,
381
- parseTimestamp,
382
- truncate,
383
- statusBadge,
384
- httpStatusColor,
385
- durationColor,
386
- };
@@ -1,233 +0,0 @@
1
- # SecureNow API Keys
2
-
3
- API keys provide programmatic access to the SecureNow platform. They support granular feature-level permissions, application scoping, IP allowlisting, and secure one-time-copy generation.
4
-
5
- ---
6
-
7
- ## Creating an API Key
8
-
9
- ### From the Dashboard
10
-
11
- 1. Go to **Settings → API Keys**
12
- 2. Click **Create API Key**
13
- 3. Enter a name (e.g., "Production Firewall", "CI/CD Pipeline")
14
- 4. Select the scopes (permissions) you need
15
- 5. Optionally restrict to specific applications and IP addresses
16
- 6. Click **Create**
17
- 7. **Copy the key immediately** — it will only be shown once
18
-
19
- ### From the CLI
20
-
21
- ```bash
22
- npx securenow login
23
- npx securenow firewall status
24
- ```
25
-
26
- The `securenow init` and `securenow login` commands can automatically provision a firewall API key for you.
27
-
28
- ---
29
-
30
- ## Key Format
31
-
32
- All API keys use the format:
33
-
34
- ```
35
- snk_live_<64 hex characters>
36
- ```
37
-
38
- Example: `snk_live_a1b2c3d4e5f6...`
39
-
40
- The `snk_live_` prefix makes it easy to identify SecureNow keys in your codebase and credential scanners.
41
-
42
- ---
43
-
44
- ## Scopes (Permissions)
45
-
46
- Each API key has a set of scopes that control what it can access. Scopes follow the `resource:action` pattern.
47
-
48
- | Scope | Description |
49
- |-------|-------------|
50
- | `firewall:read` | Read the blocklist (used by the firewall SDK) |
51
- | `blocklist:read` | List and check blocked IPs |
52
- | `blocklist:write` | Add and remove blocked IPs |
53
- | `applications:read` | List and view applications |
54
- | `applications:write` | Create and delete applications |
55
- | `traces:read` | Query traces |
56
- | `logs:read` | Query logs |
57
- | `issues:read` | List and view security issues |
58
- | `issues:write` | Resolve and manage issues |
59
- | `alerts:read` | View alert rules, channels, and history |
60
- | `alerts:write` | Create and manage alert rules |
61
- | `analytics:read` | View analytics data |
62
- | `forensics:read` | Run forensic queries |
63
- | `ip:read` | IP intelligence lookups |
64
- | `trusted:read` | List trusted IPs |
65
- | `trusted:write` | Manage trusted IPs |
66
- | `notifications:read` | List notifications |
67
- | `notifications:write` | Mark notifications as read |
68
- | `api-map:read` | View API map |
69
- | `instances:read` | List instances |
70
- | `false-positives:read` | List false positive rules |
71
- | `false-positives:write` | Create and manage false positive rules |
72
-
73
- ### Principle of Least Privilege
74
-
75
- Only grant the scopes your use case requires:
76
-
77
- - **Firewall SDK:** `firewall:read`
78
- - **CI/CD monitoring:** `issues:read`, `traces:read`
79
- - **Automated remediation:** `blocklist:read`, `blocklist:write`
80
- - **Dashboard integration:** all `*:read` scopes
81
-
82
- ---
83
-
84
- ## Application Scoping
85
-
86
- Restrict an API key to specific applications. When set, the key can only access data for those applications.
87
-
88
- Leave empty to allow access to all applications on your account.
89
-
90
- **Alert rules:** Keys that are scoped to specific applications **cannot** create or update alert rules with **`applicationsAll: true`** (“all applications”). Use explicit app keys on each rule instead. Unscoped keys may use all-apps mode.
91
-
92
- ---
93
-
94
- ## IP Allowlisting
95
-
96
- Restrict an API key to specific client IPs or CIDR ranges. When set, requests from other IPs are rejected with 403.
97
-
98
- ```
99
- 34.56.78.90
100
- 10.0.0.0/24
101
- ```
102
-
103
- Leave empty to allow from any IP.
104
-
105
- ---
106
-
107
- ## Using API Keys
108
-
109
- ### In HTTP Requests
110
-
111
- Pass the API key in the `Authorization` header:
112
-
113
- ```bash
114
- curl -s https://api.securenow.ai/api/v1/blocklist \
115
- -H "Authorization: Bearer snk_live_abc123..."
116
- ```
117
-
118
- ### In the Firewall SDK
119
-
120
- Set the `SECURENOW_API_KEY` environment variable:
121
-
122
- ```bash
123
- SECURENOW_API_KEY=snk_live_abc123...
124
- ```
125
-
126
- The firewall SDK reads this automatically on startup.
127
-
128
- ### In CI/CD
129
-
130
- ```yaml
131
- # GitHub Actions example — SDK firewall key
132
- env:
133
- SECURENOW_API_KEY: ${{ secrets.SECURENOW_API_KEY }}
134
-
135
- steps:
136
- - run: |
137
- ISSUES=$(curl -s https://api.securenow.ai/api/v1/issues \
138
- -H "Authorization: Bearer $SECURENOW_API_KEY")
139
- echo "$ISSUES" | jq '.issues | length'
140
- ```
141
-
142
- ### CLI Authentication in CI/CD
143
-
144
- For CLI commands in CI, use the `SECURENOW_TOKEN` env var to skip file-based login:
145
-
146
- ```yaml
147
- # GitHub Actions example — CLI auth via env var
148
- env:
149
- SECURENOW_TOKEN: ${{ secrets.SECURENOW_CLI_TOKEN }}
150
-
151
- steps:
152
- - run: npx securenow issues --json --status open
153
- - run: npx securenow forensics "critical attacks in last 24h" --json
154
- ```
155
-
156
- The `SECURENOW_TOKEN` env var takes priority over any stored credentials.
157
-
158
- ---
159
-
160
- ## Key Management
161
-
162
- ### Viewing Keys
163
-
164
- ```bash
165
- # From the CLI
166
- npx securenow api-keys list
167
- ```
168
-
169
- Or go to **Settings → API Keys** in the dashboard. You'll see the key name, last 4 characters, status, scopes, and last used timestamp.
170
-
171
- ### Revoking a Key
172
-
173
- ```bash
174
- npx securenow api-keys revoke <key-id>
175
- ```
176
-
177
- Or click **Revoke** in the dashboard. Revoked keys immediately stop working. This cannot be undone.
178
-
179
- ### Regenerating a Key
180
-
181
- Regeneration creates a new key with the same name, scopes, and settings. The old key is automatically revoked.
182
-
183
- ---
184
-
185
- ## Rate Limits
186
-
187
- API keys are subject to rate limiting:
188
-
189
- | Endpoint | Limit |
190
- |----------|-------|
191
- | General API (`/api/*`) | 600 requests/minute |
192
- | Firewall sync (`/api/firewall/*`) | 120 requests/minute |
193
-
194
- Rate limit headers are included in every response:
195
-
196
- ```
197
- X-RateLimit-Limit: 600
198
- X-RateLimit-Remaining: 597
199
- X-RateLimit-Reset: 1712534400
200
- ```
201
-
202
- ---
203
-
204
- ## Security Best Practices
205
-
206
- 1. **Never commit API keys to source control.** Use `.env` files or secret managers.
207
- 2. **Use the minimum scopes required.** A firewall key only needs `firewall:read`.
208
- 3. **Restrict by IP when possible.** Server keys should be locked to your server IPs.
209
- 4. **Rotate keys periodically.** Use the regenerate feature to rotate without downtime.
210
- 5. **Monitor usage.** Check the "last used" timestamp and known IPs in the dashboard.
211
- 6. **Revoke unused keys.** Delete keys that are no longer in use.
212
-
213
- ---
214
-
215
- ## API Versioning
216
-
217
- All API endpoints are available at both `/api/` and `/api/v1/`. We recommend using the versioned path for stability:
218
-
219
- ```bash
220
- # Recommended
221
- https://api.securenow.ai/api/v1/blocklist
222
-
223
- # Also works (unversioned)
224
- https://api.securenow.ai/api/blocklist
225
- ```
226
-
227
- ---
228
-
229
- ## Related Documentation
230
-
231
- - [Firewall Guide](./FIREWALL-GUIDE.md) — Automatic IP blocking setup
232
- - [Environment Variables Reference](./ENVIRONMENT-VARIABLES.md) — All configuration options
233
- - [All Frameworks Quick Start](./ALL-FRAMEWORKS-QUICKSTART.md) — Framework setup guides