securenow 5.15.0 → 5.16.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/NPM_README.md CHANGED
@@ -148,7 +148,10 @@ npx securenow login
148
148
  # Or use a token for CI/headless environments
149
149
  npx securenow login --token <YOUR_JWT>
150
150
 
151
- # Check who you're logged in as
151
+ # Log in for this project only (per-project credentials)
152
+ npx securenow login --local
153
+
154
+ # Check who you're logged in as (shows auth source)
152
155
  npx securenow whoami
153
156
  ```
154
157
 
@@ -241,6 +244,9 @@ npx securenow notifications read-all
241
244
  ```bash
242
245
  # View alert rules, channels, and history
243
246
  npx securenow alerts rules
247
+ npx securenow alerts rules show <rule-id>
248
+ npx securenow alerts rules update <rule-id> --applications-all
249
+ npx securenow alerts rules update <rule-id> --apps key1,key2
244
250
  npx securenow alerts channels
245
251
  npx securenow alerts history --limit 20
246
252
  ```
@@ -324,12 +330,15 @@ npx securenow config set format json
324
330
  npx securenow config path
325
331
  ```
326
332
 
327
- Config files are stored in `~/.securenow/`:
333
+ Config files are stored in `~/.securenow/` (global) or `.securenow/` in the project root (per-project):
328
334
 
329
335
  | File | Description |
330
336
  |------|-------------|
331
- | `config.json` | API URL, default app, output format |
332
- | `credentials.json` | Auth token (file permissions: 0600) |
337
+ | `~/.securenow/config.json` | API URL, default app, output format |
338
+ | `~/.securenow/credentials.json` | Auth token — global (file permissions: 0600) |
339
+ | `.securenow/credentials.json` | Auth token — project-local (use `login --local`) |
340
+
341
+ **Resolution order:** `SECURENOW_TOKEN` env var → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`.
333
342
 
334
343
  ### Global Flags
335
344
 
@@ -345,14 +354,37 @@ Every command supports these flags:
345
354
 
346
355
  | Variable | Description |
347
356
  |----------|-------------|
357
+ | `SECURENOW_TOKEN` | JWT token — overrides all file-based credentials |
348
358
  | `SECURENOW_API_URL` | Override the API base URL |
349
359
  | `SECURENOW_DEBUG` | Show stack traces on errors |
350
360
  | `NO_COLOR` | Disable colored output |
351
361
 
362
+ ### Multi-Project Sessions
363
+
364
+ Use `--local` to maintain separate logins per project on the same machine:
365
+
366
+ ```bash
367
+ # In project A — log in as user-a@company.com
368
+ cd ~/projects/project-a
369
+ npx securenow login --local
370
+
371
+ # In project B — log in as user-b@company.com
372
+ cd ~/projects/project-b
373
+ npx securenow login --local
374
+
375
+ # Each project uses its own credentials independently
376
+ npx securenow whoami # Shows auth source: project (.securenow/)
377
+ ```
378
+
379
+ You can also use the `SECURENOW_TOKEN` env var for per-terminal sessions without touching any files.
380
+
352
381
  ### CI/CD Integration
353
382
 
354
383
  ```bash
355
- # Authenticate with a token in CI
384
+ # Authenticate with a token in CI (env var — no file needed)
385
+ SECURENOW_TOKEN=$MY_SECRET npx securenow issues --json
386
+
387
+ # Or use login with explicit token
356
388
  npx securenow login --token $SECURENOW_TOKEN
357
389
 
358
390
  # Use --json for machine-readable output
@@ -372,9 +404,9 @@ fi
372
404
  | Category | Command | Description |
373
405
  |----------|---------|-------------|
374
406
  | **Setup** | `init` | Auto-scaffold instrumentation for your framework |
375
- | **Auth** | `login` | Authenticate via browser or `--token` |
376
- | | `logout` | Clear credentials |
377
- | | `whoami` | Show session info |
407
+ | **Auth** | `login` | Authenticate via browser, `--token`, or `--local` |
408
+ | | `logout` | Clear credentials (`--local` for project only) |
409
+ | | `whoami` | Show session info and auth source |
378
410
  | **Apps** | `apps` | List applications |
379
411
  | | `apps create <name>` | Create application |
380
412
  | | `apps info <id>` | Application details |
@@ -394,7 +426,9 @@ fi
394
426
  | | `notifications unread` | Unread count |
395
427
  | | `notifications read <id>` | Mark read |
396
428
  | | `notifications read-all` | Mark all read |
397
- | | `alerts rules` | Alert rules |
429
+ | | `alerts rules` | List rules (status, apps, schedule) |
430
+ | | `alerts rules show <id>` | Rule detail |
431
+ | | `alerts rules update <id> --applications-all` / `--apps k1,k2` | Application scope |
398
432
  | | `alerts channels` | Alert channels |
399
433
  | | `alerts history` | Alert history |
400
434
  | **Investigate** | `ip <addr>` | IP intelligence |
package/README.md CHANGED
@@ -264,8 +264,10 @@ Most users won't need this — just add `-r securenow/register` to your existing
264
264
  |---------|-------------|
265
265
  | `securenow login` | Log in via browser (opens OAuth flow) |
266
266
  | `securenow login --token <TOKEN>` | Log in with a token (for CI/headless) |
267
+ | `securenow login --local` | Log in and save credentials to the current project only |
267
268
  | `securenow logout` | Clear stored credentials |
268
- | `securenow whoami` | Show current session info |
269
+ | `securenow logout --local` | Clear project-local credentials only |
270
+ | `securenow whoami` | Show current session info (including auth source) |
269
271
 
270
272
  ### Applications
271
273
 
@@ -300,7 +302,10 @@ Most users won't need this — just add `-r securenow/register` to your existing
300
302
  | `securenow notifications unread` | Show unread count |
301
303
  | `securenow notifications read <id>` | Mark notification as read |
302
304
  | `securenow notifications read-all` | Mark all as read |
303
- | `securenow alerts rules` | List alert rules |
305
+ | `securenow alerts rules` | List alert rules (status, applications, schedule) |
306
+ | `securenow alerts rules show <id>` | Show one rule (includes all-apps vs explicit apps) |
307
+ | `securenow alerts rules update <id> --applications-all` | Set rule to all current & future apps |
308
+ | `securenow alerts rules update <id> --apps k1,k2` | Scope rule to specific app keys |
304
309
  | `securenow alerts channels` | List alert channels |
305
310
  | `securenow alerts history` | View alert history |
306
311
 
@@ -353,15 +358,19 @@ Most users won't need this — just add `-r securenow/register` to your existing
353
358
  | `--json` | Output as JSON (works on every command) |
354
359
  | `--help` | Show help for any command |
355
360
  | `--app <key>` | Specify app key (or set default with `config set defaultApp`) |
361
+ | `--local` | Save/clear credentials per-project (login/logout only) |
356
362
 
357
363
  ### Configuration
358
364
 
359
- Credentials and settings are stored in `~/.securenow/`:
365
+ Credentials and settings are stored in `~/.securenow/` (global) or `.securenow/` (per-project):
360
366
 
361
367
  | File | Purpose |
362
368
  |------|---------|
363
369
  | `~/.securenow/config.json` | API URL, default app, preferences |
364
- | `~/.securenow/credentials.json` | Auth token (restricted permissions) |
370
+ | `~/.securenow/credentials.json` | Auth token — global (restricted permissions) |
371
+ | `.securenow/credentials.json` | Auth token — project-local (use `login --local`) |
372
+
373
+ **Credential resolution order:** `SECURENOW_TOKEN` env var → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`.
365
374
 
366
375
  Override the API URL with `securenow config set apiUrl <url>` or the `SECURENOW_API_URL` environment variable.
367
376
 
package/SKILL-CLI.md CHANGED
@@ -18,9 +18,12 @@ npx securenow <command>
18
18
  ```bash
19
19
  securenow login # opens browser OAuth; stores JWT in ~/.securenow/credentials.json
20
20
  securenow login --token <JWT> # headless / CI login (get token from dashboard Settings)
21
- securenow whoami # verify session
21
+ securenow login --local # save credentials to this project only (.securenow/)
22
+ securenow whoami # verify session (shows auth source)
22
23
  ```
23
24
 
25
+ **Per-project credentials:** Use `--local` to keep separate logins in different project directories on the same machine. Credentials resolve in order: `SECURENOW_TOKEN` env var → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`.
26
+
24
27
  ### Integrate With Your App
25
28
 
26
29
  The CLI can also instrument any Node.js app at launch — no code changes:
@@ -47,22 +50,25 @@ Save this file as `.cursor/skills/securenow-cli/SKILL.md` in your project. Your
47
50
 
48
51
  ## Configuration
49
52
 
50
- Config lives in `~/.securenow/`:
53
+ Config lives in `~/.securenow/` (global) and optionally `.securenow/` (per-project):
51
54
 
52
55
  | File | Content |
53
56
  |------|---------|
54
- | `config.json` | `apiUrl`, `appUrl`, `defaultApp`, `output` |
55
- | `credentials.json` | `token`, `email`, `expiresAt` |
57
+ | `~/.securenow/config.json` | `apiUrl`, `appUrl`, `defaultApp`, `output` |
58
+ | `~/.securenow/credentials.json` | `token`, `email`, `expiresAt` (global) |
59
+ | `.securenow/credentials.json` | `token`, `email`, `expiresAt` (project-local, use `login --local`) |
60
+
61
+ **Credential resolution order:** `SECURENOW_TOKEN` env var → `.securenow/credentials.json` (project) → `~/.securenow/credentials.json` (global).
56
62
 
57
63
  ```bash
58
64
  securenow config set apiUrl https://api.securenow.ai
59
65
  securenow config set defaultApp my-app-key
60
66
  securenow config get # show all
61
67
  securenow config get defaultApp # show one
62
- securenow config path # print file paths
68
+ securenow config path # print file paths + active auth source
63
69
  ```
64
70
 
65
- Environment overrides: `SECURENOW_API_URL`, `SECURENOW_APP_URL`, `SECURENOW_APP` (default app key).
71
+ Environment overrides: `SECURENOW_TOKEN` (JWT), `SECURENOW_API_URL`, `SECURENOW_APP_URL`, `SECURENOW_APP` (default app key).
66
72
 
67
73
  ## Global Flags
68
74
 
@@ -94,10 +100,12 @@ Spawns `node --require securenow/register [--import otel/hook.mjs] <script>`. ES
94
100
  ### Authentication
95
101
 
96
102
  ```bash
97
- securenow login # browser-based OAuth (starts local callback server)
103
+ securenow login # browser-based OAuth (stores in global ~/.securenow/)
98
104
  securenow login --token <JWT> # headless / CI login
99
- securenow logout # clear ~/.securenow/credentials.json
100
- securenow whoami # show email, user ID, API URL, expiry, default app
105
+ securenow login --local # save credentials to project .securenow/ (per-project session)
106
+ securenow logout # clear active credentials (local if present, else global)
107
+ securenow logout --local # clear project-local credentials only
108
+ securenow whoami # show email, user ID, API URL, auth source, expiry, default app
101
109
  ```
102
110
 
103
111
  ### Applications
@@ -174,7 +182,10 @@ securenow notifications unread # unread count
174
182
 
175
183
  ```bash
176
184
  securenow alerts # list alert rules (default)
177
- securenow alerts rules # list alert rules
185
+ securenow alerts rules # list alert rules (columns: Status, Applications, Schedule)
186
+ securenow alerts rules show <id> # one rule; JSON: --json
187
+ securenow alerts rules update <id> --applications-all # all current & future apps
188
+ securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
178
189
  securenow alerts channels # list alert channels (Slack, email, etc.)
179
190
  securenow alerts history [--limit N] # past triggered alerts
180
191
  ```
@@ -386,8 +397,8 @@ All commands support `--json` for structured output. When piping to other tools
386
397
 
387
398
  | Exit code / Error | Meaning | Recovery |
388
399
  |------------------|---------|----------|
389
- | `Session expired` | JWT expired | `securenow login` |
390
- | `Not logged in` | No token in `~/.securenow/credentials.json` | `securenow login` |
400
+ | `Session expired` | JWT expired | `securenow login` (or `login --local`) |
401
+ | `Not logged in` | No token found | `securenow login` or set `SECURENOW_TOKEN` env var |
391
402
  | `Access denied (403)` | Insufficient plan or permissions | Upgrade plan or check user role |
392
403
  | `Cannot connect` | API unreachable | Check `SECURENOW_API_URL` or network |
393
404
  | `Unknown command` | Typo or unrecognized command | `securenow help` |
package/cli/auth.js CHANGED
@@ -120,6 +120,8 @@ async function loginWithToken(token) {
120
120
  }
121
121
 
122
122
  async function login(args, flags) {
123
+ const local = !!flags.local;
124
+
123
125
  if (flags.token) {
124
126
  const token = flags.token;
125
127
  await loginWithToken(token);
@@ -127,9 +129,11 @@ async function login(args, flags) {
127
129
  const email = payload?.email || 'unknown';
128
130
  const exp = payload?.exp ? payload.exp * 1000 : null;
129
131
 
130
- config.setAuth(token, email, exp);
132
+ config.setAuth(token, email, exp, { local });
133
+ if (local) config.ensureLocalGitignore();
131
134
  console.log('');
132
135
  ui.success(`Logged in as ${ui.c.bold(email)}`);
136
+ if (local) ui.info('Credentials saved to project .securenow/ (local)');
133
137
  if (exp) {
134
138
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
135
139
  ui.info(`Session expires in ${days} days`);
@@ -143,9 +147,11 @@ async function login(args, flags) {
143
147
  const email = payload?.email || 'unknown';
144
148
  const exp = payload?.exp ? payload.exp * 1000 : null;
145
149
 
146
- config.setAuth(token, email, exp);
150
+ config.setAuth(token, email, exp, { local });
151
+ if (local) config.ensureLocalGitignore();
147
152
  console.log('');
148
153
  ui.success(`Logged in as ${ui.c.bold(email)}`);
154
+ if (local) ui.info('Credentials saved to project .securenow/ (local)');
149
155
  if (exp) {
150
156
  const days = Math.ceil((exp - Date.now()) / (1000 * 60 * 60 * 24));
151
157
  ui.info(`Session expires in ${days} days`);
@@ -165,14 +171,16 @@ async function login(args, flags) {
165
171
  }
166
172
  }
167
173
 
168
- async function logout() {
174
+ async function logout(args, flags) {
175
+ const local = flags ? flags.local : undefined;
169
176
  const creds = config.loadCredentials();
170
- config.clearCredentials();
177
+ config.clearCredentials({ local });
171
178
  if (creds.email) {
172
179
  ui.success(`Logged out from ${ui.c.bold(creds.email)}`);
173
180
  } else {
174
181
  ui.success('Logged out');
175
182
  }
183
+ if (local) ui.info('Cleared project-local credentials');
176
184
  }
177
185
 
178
186
  async function whoami() {
@@ -192,6 +200,7 @@ async function whoami() {
192
200
  ['Email', creds.email || payload?.email || 'unknown'],
193
201
  ['User ID', payload?.sub || 'unknown'],
194
202
  ['API', config.getApiUrl()],
203
+ ['Auth Source', config.getAuthSource()],
195
204
  ];
196
205
  if (creds.expiresAt) {
197
206
  const days = Math.ceil((creds.expiresAt - Date.now()) / (1000 * 60 * 60 * 24));
package/cli/config.js CHANGED
@@ -8,6 +8,10 @@ const CONFIG_DIR = path.join(os.homedir(), '.securenow');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
  const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
10
10
 
11
+ const LOCAL_CONFIG_DIR = path.join(process.cwd(), '.securenow');
12
+ const LOCAL_CONFIG_FILE = path.join(LOCAL_CONFIG_DIR, 'config.json');
13
+ const LOCAL_CREDENTIALS_FILE = path.join(LOCAL_CONFIG_DIR, 'credentials.json');
14
+
11
15
  const DEFAULTS = {
12
16
  apiUrl: 'https://api.securenow.ai',
13
17
  appUrl: 'https://app.securenow.ai',
@@ -15,9 +19,9 @@ const DEFAULTS = {
15
19
  output: 'table',
16
20
  };
17
21
 
18
- function ensureDir() {
19
- if (!fs.existsSync(CONFIG_DIR)) {
20
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
22
+ function ensureDir(dir) {
23
+ if (!fs.existsSync(dir || CONFIG_DIR)) {
24
+ fs.mkdirSync(dir || CONFIG_DIR, { recursive: true, mode: 0o700 });
21
25
  }
22
26
  }
23
27
 
@@ -30,7 +34,7 @@ function loadJSON(filepath) {
30
34
  }
31
35
 
32
36
  function saveJSON(filepath, data) {
33
- ensureDir();
37
+ ensureDir(path.dirname(filepath));
34
38
  fs.writeFileSync(filepath, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
35
39
  if (process.platform === 'win32') {
36
40
  try {
@@ -40,8 +44,25 @@ function saveJSON(filepath, data) {
40
44
  }
41
45
  }
42
46
 
47
+ function hasLocalCredentials() {
48
+ return fs.existsSync(LOCAL_CREDENTIALS_FILE);
49
+ }
50
+
51
+ function resolveCredentialsFile() {
52
+ if (fs.existsSync(LOCAL_CREDENTIALS_FILE)) return LOCAL_CREDENTIALS_FILE;
53
+ return CREDENTIALS_FILE;
54
+ }
55
+
56
+ function getAuthSource() {
57
+ if (process.env.SECURENOW_TOKEN) return 'env (SECURENOW_TOKEN)';
58
+ if (fs.existsSync(LOCAL_CREDENTIALS_FILE)) return 'project (.securenow/)';
59
+ return 'global (~/.securenow/)';
60
+ }
61
+
43
62
  function loadConfig() {
44
- return { ...DEFAULTS, ...loadJSON(CONFIG_FILE) };
63
+ const global = loadJSON(CONFIG_FILE);
64
+ const local = fs.existsSync(LOCAL_CONFIG_FILE) ? loadJSON(LOCAL_CONFIG_FILE) : {};
65
+ return { ...DEFAULTS, ...global, ...local };
45
66
  }
46
67
 
47
68
  function saveConfig(config) {
@@ -59,20 +80,29 @@ function setConfigValue(key, value) {
59
80
  }
60
81
 
61
82
  function loadCredentials() {
62
- return loadJSON(CREDENTIALS_FILE);
83
+ return loadJSON(resolveCredentialsFile());
63
84
  }
64
85
 
65
- function saveCredentials(creds) {
66
- saveJSON(CREDENTIALS_FILE, creds);
86
+ function saveCredentials(creds, { local = false } = {}) {
87
+ const targetFile = local ? LOCAL_CREDENTIALS_FILE : CREDENTIALS_FILE;
88
+ saveJSON(targetFile, creds);
67
89
  }
68
90
 
69
- function clearCredentials() {
91
+ function clearCredentials({ local } = {}) {
70
92
  try {
71
- fs.unlinkSync(CREDENTIALS_FILE);
93
+ if (local === true) {
94
+ fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
95
+ } else if (local === false || !hasLocalCredentials()) {
96
+ fs.unlinkSync(CREDENTIALS_FILE);
97
+ } else {
98
+ fs.unlinkSync(LOCAL_CREDENTIALS_FILE);
99
+ }
72
100
  } catch {}
73
101
  }
74
102
 
75
103
  function getToken() {
104
+ if (process.env.SECURENOW_TOKEN) return process.env.SECURENOW_TOKEN;
105
+
76
106
  const creds = loadCredentials();
77
107
  if (!creds.token) return null;
78
108
 
@@ -82,8 +112,23 @@ function getToken() {
82
112
  return creds.token;
83
113
  }
84
114
 
85
- function setAuth(token, email, expiresAt) {
86
- saveCredentials({ token, email, expiresAt });
115
+ function setAuth(token, email, expiresAt, { local = false } = {}) {
116
+ saveCredentials({ token, email, expiresAt }, { local });
117
+ }
118
+
119
+ function ensureLocalGitignore() {
120
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
121
+ const entry = '.securenow/';
122
+ try {
123
+ if (fs.existsSync(gitignorePath)) {
124
+ const content = fs.readFileSync(gitignorePath, 'utf8');
125
+ if (!content.split('\n').some(line => line.trim() === entry)) {
126
+ fs.appendFileSync(gitignorePath, `\n# SecureNow local credentials\n${entry}\n`);
127
+ }
128
+ } else {
129
+ fs.writeFileSync(gitignorePath, `# SecureNow local credentials\n${entry}\n`);
130
+ }
131
+ } catch {}
87
132
  }
88
133
 
89
134
  function getApiUrl() {
@@ -102,6 +147,8 @@ module.exports = {
102
147
  CONFIG_DIR,
103
148
  CONFIG_FILE,
104
149
  CREDENTIALS_FILE,
150
+ LOCAL_CONFIG_DIR,
151
+ LOCAL_CREDENTIALS_FILE,
105
152
  loadConfig,
106
153
  saveConfig,
107
154
  getConfigValue,
@@ -111,6 +158,9 @@ module.exports = {
111
158
  clearCredentials,
112
159
  getToken,
113
160
  setAuth,
161
+ getAuthSource,
162
+ hasLocalCredentials,
163
+ ensureLocalGitignore,
114
164
  getApiUrl,
115
165
  getAppUrl,
116
166
  getDefaultApp,
package/cli/security.js CHANGED
@@ -10,6 +10,39 @@ function resolveApp(flags) {
10
10
 
11
11
  // ── Alert Rules ──
12
12
 
13
+ function formatRuleApplicationsCell(rule) {
14
+ if (rule.applicationsAll) {
15
+ return ui.c.cyan('all apps');
16
+ }
17
+ const keys = rule.applications || [];
18
+ if (keys.length === 0) return ui.c.dim('—');
19
+ const joined = keys.join(', ');
20
+ return joined.length > 48 ? `${joined.slice(0, 45)}…` : joined;
21
+ }
22
+
23
+ function ruleStatusBadge(rule) {
24
+ const st = rule.status || (rule.enabled !== false ? 'Active' : 'Disabled');
25
+ if (st === 'Active') return ui.statusBadge('active');
26
+ if (st === 'Disabled') return ui.statusBadge('disabled');
27
+ if (st === 'Paused') return ui.statusBadge('paused');
28
+ return st;
29
+ }
30
+
31
+ /** Dispatch: list | show <id> | update <id> ... */
32
+ async function alertRulesRoute(args, flags) {
33
+ const sub = args[0];
34
+ if (sub === 'show') {
35
+ return alertRuleShow(args.slice(1), flags);
36
+ }
37
+ if (sub === 'update') {
38
+ return alertRuleUpdate(args.slice(1), flags);
39
+ }
40
+ if (sub === 'list') {
41
+ return alertRulesList(args.slice(1), flags);
42
+ }
43
+ return alertRulesList(args, flags);
44
+ }
45
+
13
46
  async function alertRulesList(args, flags) {
14
47
  requireAuth();
15
48
  const s = ui.spinner('Fetching alert rules');
@@ -21,15 +54,16 @@ async function alertRulesList(args, flags) {
21
54
  if (flags.json) { ui.json(rules); return; }
22
55
 
23
56
  console.log('');
24
- const rows = rules.map(r => [
57
+ const rows = rules.map((r) => [
25
58
  ui.c.dim(ui.truncate(r._id, 12)),
26
- r.name || r.type || '—',
27
- ui.statusBadge(r.enabled !== false ? 'enabled' : 'disabled'),
28
- r.severity ? ui.statusBadge(r.severity) : '—',
29
- r.type || '—',
30
- r.serviceName || ui.c.dim('all'),
59
+ r.name || '—',
60
+ ruleStatusBadge(r),
61
+ formatRuleApplicationsCell(r),
62
+ r.schedule?.enabled === false ? ui.c.dim('off') : (r.schedule?.description || r.schedule?.cronExpression || '—'),
31
63
  ]);
32
- ui.table(['ID', 'Name', 'Status', 'Severity', 'Type', 'App'], rows);
64
+ ui.table(['ID', 'Name', 'Status', 'Applications', 'Schedule'], rows);
65
+ console.log('');
66
+ console.log(ui.c.dim(' Applications: "all apps" = all current & future active apps. show <id> · update <id> --applications-all · --apps k1,k2'));
33
67
  console.log('');
34
68
  } catch (err) {
35
69
  s.fail('Failed to fetch alert rules');
@@ -37,6 +71,105 @@ async function alertRulesList(args, flags) {
37
71
  }
38
72
  }
39
73
 
74
+ async function alertRuleShow(args, flags) {
75
+ requireAuth();
76
+ const id = args[0];
77
+ if (!id) {
78
+ ui.error('Usage: securenow alerts rules show <rule-id>');
79
+ process.exit(1);
80
+ }
81
+ const s = ui.spinner('Fetching alert rule');
82
+ try {
83
+ const data = await api.get(`/alert-rules/${id}`);
84
+ const r = data.alertRule;
85
+ s.stop('');
86
+
87
+ if (flags.json) {
88
+ ui.json(r);
89
+ return;
90
+ }
91
+
92
+ console.log('');
93
+ ui.heading(r.name || 'Alert rule');
94
+ console.log('');
95
+ const appLine = r.applicationsAll
96
+ ? 'All applications (current & future)'
97
+ : (r.applications && r.applications.length > 0 ? r.applications.join(', ') : '—');
98
+ ui.keyValue([
99
+ ['ID', r._id || r.id || id],
100
+ ['Status', r.status || '—'],
101
+ ['System rule', r.isSystem ? 'yes' : 'no'],
102
+ ['Applications', appLine],
103
+ ['Schedule', r.schedule?.enabled === false ? 'disabled' : (r.schedule?.description || r.schedule?.cronExpression || '—')],
104
+ ['Throttle', r.throttle?.enabled ? `${r.throttle.minutes} min` : 'off'],
105
+ ['Query', r.queryMappingId?.name || '—'],
106
+ ]);
107
+ console.log('');
108
+ } catch (err) {
109
+ s.fail('Failed to fetch alert rule');
110
+ throw err;
111
+ }
112
+ }
113
+
114
+ async function alertRuleUpdate(args, flags) {
115
+ requireAuth();
116
+ const id = args[0];
117
+ if (!id) {
118
+ ui.error('Usage: securenow alerts rules update <rule-id> (--applications-all | --apps <k1,k2>)');
119
+ process.exit(1);
120
+ }
121
+
122
+ const hasAll =
123
+ flags['applications-all'] === true ||
124
+ flags['applications-all'] === 'true';
125
+ const hasNoAll =
126
+ flags['no-applications-all'] === true ||
127
+ flags['no-applications-all'] === 'true';
128
+ const appsStr = flags.apps;
129
+
130
+ if (hasAll && hasNoAll) {
131
+ ui.error('Use only one of --applications-all or --no-applications-all');
132
+ process.exit(1);
133
+ }
134
+ if (hasAll && appsStr) {
135
+ ui.error('Cannot combine --applications-all with --apps');
136
+ process.exit(1);
137
+ }
138
+
139
+ const body = {};
140
+ if (hasAll) {
141
+ body.applicationsAll = true;
142
+ body.applications = [];
143
+ } else if (hasNoAll || appsStr) {
144
+ body.applicationsAll = false;
145
+ if (!appsStr) {
146
+ ui.error('Pass --apps key1,key2 when using --no-applications-all, or omit --no-applications-all and use --apps only');
147
+ process.exit(1);
148
+ }
149
+ body.applications = String(appsStr)
150
+ .split(',')
151
+ .map((x) => x.trim())
152
+ .filter(Boolean);
153
+ if (body.applications.length === 0) {
154
+ ui.error('No application keys parsed from --apps');
155
+ process.exit(1);
156
+ }
157
+ } else {
158
+ ui.error('Nothing to update. Use --applications-all or --apps key1,key2');
159
+ process.exit(1);
160
+ }
161
+
162
+ const s = ui.spinner('Updating alert rule');
163
+ try {
164
+ await api.put(`/alert-rules/${id}`, body);
165
+ s.stop('Updated');
166
+ ui.success('Alert rule updated');
167
+ } catch (err) {
168
+ s.fail('Failed to update alert rule');
169
+ throw err;
170
+ }
171
+ }
172
+
40
173
  // ── Alert Channels ──
41
174
 
42
175
  async function alertChannelsList(args, flags) {
@@ -901,7 +1034,10 @@ async function analytics(args, flags) {
901
1034
  }
902
1035
 
903
1036
  module.exports = {
1037
+ alertRulesRoute,
904
1038
  alertRulesList,
1039
+ alertRuleShow,
1040
+ alertRuleUpdate,
905
1041
  alertChannelsList,
906
1042
  alertHistoryList,
907
1043
  blocklistList,
package/cli/ui.js CHANGED
@@ -302,6 +302,7 @@ function statusBadge(status) {
302
302
  trusted: c.green('■ trusted'),
303
303
  enabled: c.green('● enabled'),
304
304
  disabled: c.dim('○ disabled'),
305
+ paused: c.yellow('◆ paused'),
305
306
  critical: c.red('▲ critical'),
306
307
  high: c.red('▲ high'),
307
308
  medium: c.yellow('▲ medium'),
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');
@@ -47,14 +47,15 @@ const COMMANDS = {
47
47
  },
48
48
  login: {
49
49
  desc: 'Authenticate with SecureNow',
50
- usage: 'securenow login [--token <TOKEN>]',
51
- flags: { token: 'Authenticate with a token directly' },
50
+ usage: 'securenow login [--token <TOKEN>] [--local]',
51
+ flags: { token: 'Authenticate with a token directly', local: 'Save credentials to this project only (.securenow/)' },
52
52
  run: (a, f) => require('./cli/auth').login(a, f),
53
53
  },
54
54
  logout: {
55
55
  desc: 'Clear stored credentials',
56
- usage: 'securenow logout',
57
- run: () => require('./cli/auth').logout(),
56
+ usage: 'securenow logout [--local]',
57
+ flags: { local: 'Clear project-local credentials only' },
58
+ run: (a, f) => require('./cli/auth').logout(a, f),
58
59
  },
59
60
  whoami: {
60
61
  desc: 'Show current session info',
@@ -119,7 +120,16 @@ const COMMANDS = {
119
120
  desc: 'Manage alerting',
120
121
  usage: 'securenow alerts <subcommand> [options]',
121
122
  sub: {
122
- rules: { desc: 'List alert rules', run: (a, f) => require('./cli/security').alertRulesList(a, f) },
123
+ rules: {
124
+ desc: 'List, show, or update alert rules',
125
+ flags: {
126
+ json: 'Output as JSON',
127
+ 'applications-all': 'With update: scope rule to all apps',
128
+ 'no-applications-all': 'With update: scope to explicit --apps list',
129
+ apps: 'Comma-separated app keys (with update)',
130
+ },
131
+ run: (a, f) => require('./cli/security').alertRulesRoute(a, f),
132
+ },
123
133
  channels: { desc: 'List alert channels', run: (a, f) => require('./cli/security').alertChannelsList(a, f) },
124
134
  history: { desc: 'View alert history', flags: { limit: 'Max results' }, run: (a, f) => require('./cli/security').alertHistoryList(a, f) },
125
135
  },
@@ -267,8 +277,12 @@ const COMMANDS = {
267
277
  desc: 'Show config file path',
268
278
  run: () => {
269
279
  const conf = require('./cli/config');
270
- console.log(`Config: ${conf.CONFIG_FILE}`);
271
- console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
280
+ console.log(`Config: ${conf.CONFIG_FILE}`);
281
+ console.log(`Credentials: ${conf.CREDENTIALS_FILE}`);
282
+ if (conf.hasLocalCredentials()) {
283
+ console.log(`Local creds: ${conf.LOCAL_CREDENTIALS_FILE} ${require('./cli/ui').c.green('(active)')}`);
284
+ }
285
+ console.log(`Auth source: ${conf.getAuthSource()}`);
272
286
  },
273
287
  },
274
288
  },
@@ -764,10 +764,14 @@ The SecureNow CLI is your terminal command center. Below is every command organi
764
764
 
765
765
  | Command | What It Does |
766
766
  |---------|-------------|
767
- | `securenow login` | Opens browser to authenticate |
767
+ | `securenow login` | Opens browser to authenticate (global session) |
768
768
  | `securenow login --token <T>` | Authenticate with a token (for CI/CD or headless servers) |
769
+ | `securenow login --local` | Save credentials to this project only (per-project session) |
769
770
  | `securenow logout` | Clear stored credentials |
770
- | `securenow whoami` | Show current session (email, API URL, expiry, default app) |
771
+ | `securenow logout --local` | Clear project-local credentials only |
772
+ | `securenow whoami` | Show current session (email, API URL, auth source, expiry, default app) |
773
+
774
+ **Per-project credentials:** Use `--local` to maintain separate logins for different projects on the same machine. The CLI resolves credentials in order: `SECURENOW_TOKEN` env var → project `.securenow/credentials.json` → global `~/.securenow/credentials.json`.
771
775
 
772
776
  ### App Management
773
777
 
@@ -805,6 +809,9 @@ The SecureNow CLI is your terminal command center. Below is every command organi
805
809
  | `securenow issues show <id>` | Full issue detail + AI analysis |
806
810
  | `securenow issues resolve <id>` | Mark issue as resolved |
807
811
  | `securenow alerts rules` | List alert rules |
812
+ | `securenow alerts rules show <id>` | One rule (all-apps vs explicit keys) |
813
+ | `securenow alerts rules update <id> --applications-all` | All current & future apps |
814
+ | `securenow alerts rules update <id> --apps k1,k2` | Explicit app keys only |
808
815
  | `securenow alerts channels` | List alert channels (email, webhook, Slack) |
809
816
  | `securenow alerts history --limit 50` | View past triggered alerts |
810
817
  | `securenow notifications` | List notifications |
@@ -1171,6 +1178,11 @@ Configure alert rules and channels from the [dashboard](https://app.securenow.ai
1171
1178
  # List your alert rules
1172
1179
  npx securenow alerts rules
1173
1180
 
1181
+ # Show one rule / set application scope (all apps vs explicit keys)
1182
+ npx securenow alerts rules show <rule-id>
1183
+ npx securenow alerts rules update <rule-id> --applications-all
1184
+ npx securenow alerts rules update <rule-id> --apps key1,key2
1185
+
1174
1186
  # List alert channels (email, Slack, webhook)
1175
1187
  npx securenow alerts channels
1176
1188
 
@@ -1305,9 +1317,15 @@ Or re-authenticate with a token:
1305
1317
  npx securenow login --token <YOUR_TOKEN>
1306
1318
  ```
1307
1319
 
1320
+ Or set the env var directly:
1321
+
1322
+ ```bash
1323
+ SECURENOW_TOKEN=<YOUR_JWT> npx securenow whoami
1324
+ ```
1325
+
1308
1326
  ### CLI says "Session expired"
1309
1327
 
1310
- Tokens expire after a set period. Re-run `securenow login` to get a fresh session.
1328
+ Tokens expire after a set period. Re-run `securenow login` to get a fresh session. Use `securenow whoami` to check which credential source is active.
1311
1329
 
1312
1330
  ---
1313
1331
 
@@ -87,6 +87,8 @@ Restrict an API key to specific applications. When set, the key can only access
87
87
 
88
88
  Leave empty to allow access to all applications on your account.
89
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
+
90
92
  ---
91
93
 
92
94
  ## IP Allowlisting
@@ -126,7 +128,7 @@ The firewall SDK reads this automatically on startup.
126
128
  ### In CI/CD
127
129
 
128
130
  ```yaml
129
- # GitHub Actions example
131
+ # GitHub Actions example — SDK firewall key
130
132
  env:
131
133
  SECURENOW_API_KEY: ${{ secrets.SECURENOW_API_KEY }}
132
134
 
@@ -137,6 +139,22 @@ steps:
137
139
  echo "$ISSUES" | jq '.issues | length'
138
140
  ```
139
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
+
140
158
  ---
141
159
 
142
160
  ## Key Management
package/firewall.js CHANGED
@@ -395,6 +395,7 @@ p{font-size:.9rem;line-height:1.7;color:#a1a1aa;margin-bottom:.5rem}
395
395
  .contact a{color:#f87171;text-decoration:none;font-weight:500}
396
396
  .contact a:hover{text-decoration:underline}
397
397
  .footer{margin-top:2rem;font-size:.7rem;color:#3f3f46}
398
+ .powered{margin-top:1.5rem;font-size:.75rem;color:#52525b}.powered a{color:#a1a1aa;text-decoration:none;font-weight:600;transition:color .2s}.powered a:hover{color:#f87171;text-decoration:underline}
398
399
  </style>
399
400
  </head>
400
401
  <body>
@@ -408,6 +409,7 @@ p{font-size:.9rem;line-height:1.7;color:#a1a1aa;margin-bottom:.5rem}
408
409
  <div class="divider"></div>
409
410
  <p class="contact">If you believe this is a mistake, please contact us at<br><a href="mailto:contact@securenow.ai?subject=Blocked%20IP%20Appeal%20-%20${encodeURIComponent(maskedIp)}&body=IP:%20${encodeURIComponent(maskedIp)}%0ATimestamp:%20${encodeURIComponent(new Date().toISOString())}%0A%0APlease%20describe%20why%20you%20believe%20this%20block%20is%20incorrect:">contact@securenow.ai</a><br>Include your IP address and the time of this incident.</p>
410
411
  <p class="footer">Ref: ${maskedIp} &mdash; ${new Date().toISOString()} &mdash; HTTP 403</p>
412
+ <p class="powered">Protected by <a href="https://securenow.ai" rel="dofollow" target="_blank">SecureNow</a></p>
411
413
  </div>
412
414
  </body>
413
415
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securenow",
3
- "version": "5.15.0",
3
+ "version": "5.16.0",
4
4
  "description": "OpenTelemetry instrumentation for Node.js, Next.js, and Nuxt - Send traces and logs to any OTLP-compatible backend",
5
5
  "type": "commonjs",
6
6
  "main": "register.js",