securenow 7.6.8 → 7.7.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 +12 -1
- package/SKILL-CLI.md +29 -16
- package/cli/automation.js +275 -0
- package/cli/firewall.js +29 -12
- package/cli/human.js +96 -2
- package/cli/security.js +171 -42
- package/cli.js +71 -28
- package/mcp/catalog.js +327 -15
- package/nextjs.js +22 -23
- package/nuxt-server-plugin.mjs +13 -8
- package/package.json +2 -11
- package/resolve-ip.js +135 -60
- package/tracing.js +25 -4
package/NPM_README.md
CHANGED
|
@@ -511,6 +511,9 @@ npx securenow logs --json --level error | jq '.logs'
|
|
|
511
511
|
| | `firewall test-ip <ip>` | Check if an IP would be blocked |
|
|
512
512
|
| | `run --firewall-only <script>` | Preload firewall without OTel tracing overhead |
|
|
513
513
|
| **Remediate** | `blocklist` | Blocked IPs |
|
|
514
|
+
| | `automation` | List blocklist automation rules |
|
|
515
|
+
| | `automation dry-run <id>` | Preview automation matches without writing blocks |
|
|
516
|
+
| | `automation execute <id> --yes` | Run an automation rule now |
|
|
514
517
|
| | `blocklist add <ip>` | Block IP |
|
|
515
518
|
| | `blocklist remove <id>` | Unblock IP |
|
|
516
519
|
| | `blocklist stats` | Block stats |
|
|
@@ -1652,7 +1655,15 @@ After blocking an IP, it takes 10-15 seconds to propagate (one version-check int
|
|
|
1652
1655
|
|
|
1653
1656
|
**Check 5: Are you behind a proxy?**
|
|
1654
1657
|
|
|
1655
|
-
|
|
1658
|
+
Make sure your proxy forwards the real visitor IP:
|
|
1659
|
+
|
|
1660
|
+
```nginx
|
|
1661
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1662
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
1663
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
SecureNow trusts private, loopback, and same-host proxy peers automatically. For external load balancers or public proxy IPs, add them to `config.networking.trustedProxies` in `.securenow/credentials.json`. If no forwarded header reaches the app, SecureNow can detect the attack shape but cannot recover the real visitor IP from traces.
|
|
1656
1667
|
|
|
1657
1668
|
**Check 6: Using PM2?**
|
|
1658
1669
|
|
package/SKILL-CLI.md
CHANGED
|
@@ -223,6 +223,7 @@ securenow human list --limit 20
|
|
|
223
223
|
securenow human show 1 # inspect row 1 with AI report, DAG, proofs, trace links
|
|
224
224
|
securenow human block 1 --yes --reason "AI evidence confirmed malicious"
|
|
225
225
|
securenow human fp 1 --yes --reason "Scoped false positive after evidence review"
|
|
226
|
+
securenow human action 1 --status rejected --yes --reason "Tuning guard is too broad"
|
|
226
227
|
securenow human prompt 1 # print a Codex/Claude MCP prompt for this row
|
|
227
228
|
securenow human work --limit 10 # list queue + print MCP runbook for deep queue work
|
|
228
229
|
```
|
|
@@ -234,6 +235,7 @@ MCP parity:
|
|
|
234
235
|
- `securenow_human_action_report`
|
|
235
236
|
- `securenow_human_action_block`
|
|
236
237
|
- `securenow_human_action_false_positive`
|
|
238
|
+
- `securenow_human_case_action_update`
|
|
237
239
|
- prompts: `investigate_human_action_row`, `work_human_actions`
|
|
238
240
|
|
|
239
241
|
Write tools still require `confirm:true` plus a reason. False positives should stay restrictive to the app, alert rule, path, method/status, user-agent, body pattern, or other exact evidence the AI report supports.
|
|
@@ -243,10 +245,12 @@ Write tools still require `confirm:true` plus a reason. False positives should s
|
|
|
243
245
|
```bash
|
|
244
246
|
securenow alerts # list alert rules (default)
|
|
245
247
|
securenow alerts rules # list alert rules (columns: Status, Applications, Schedule)
|
|
246
|
-
securenow alerts rules show <id> # one rule; JSON: --json
|
|
247
|
-
securenow alerts rules update <id> --applications-all # all current & future apps
|
|
248
|
-
securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
|
|
249
|
-
securenow alerts
|
|
248
|
+
securenow alerts rules show <id> # one rule; JSON: --json
|
|
249
|
+
securenow alerts rules update <id> --applications-all # all current & future apps
|
|
250
|
+
securenow alerts rules update <id> --apps key1,key2 # explicit app keys only
|
|
251
|
+
securenow alerts rules test <id> --mode dry_run --wait # validate a rule query
|
|
252
|
+
securenow alerts rules exclusions <id> list # embedded rule exclusions
|
|
253
|
+
securenow alerts channels # list alert channels (Slack, email, etc.)
|
|
250
254
|
securenow alerts history [--limit N] # past triggered alerts
|
|
251
255
|
```
|
|
252
256
|
|
|
@@ -282,9 +286,9 @@ securenow api-map stats # endpoint statistics
|
|
|
282
286
|
### Firewall
|
|
283
287
|
|
|
284
288
|
```bash
|
|
285
|
-
securenow firewall # show status (default)
|
|
286
|
-
securenow firewall status --env production #
|
|
287
|
-
securenow firewall test-ip <ip>
|
|
289
|
+
securenow firewall # show status (default)
|
|
290
|
+
securenow firewall status --app <key> --env production # app/env toggle, sync time, blocked count
|
|
291
|
+
securenow firewall test-ip <ip> --app <key> --env local # check if IP would be blocked
|
|
288
292
|
```
|
|
289
293
|
|
|
290
294
|
**Zero-config setup (v7.5.1+):** running `securenow login` enables the selected app's firewall toggle, auto-mints an API key (scoped `firewall:read + blocklist:read + allowlist:read`), and writes it to the credentials file after the app is selected. No `SECURENOW_API_KEY` env var needed. If the user already has a key, `securenow api-key set snk_live_...` achieves the same thing. See [the landing firewall page](https://securenow.ai/firewall) for an overview.
|
|
@@ -292,19 +296,28 @@ securenow firewall test-ip <ip> # check if IP would be blocked
|
|
|
292
296
|
### Blocklist — Block Malicious IPs
|
|
293
297
|
|
|
294
298
|
```bash
|
|
295
|
-
securenow blocklist # list blocked IPs
|
|
296
|
-
securenow blocklist list
|
|
297
|
-
securenow blocklist add <ip>
|
|
298
|
-
securenow blocklist remove <id>
|
|
299
|
-
securenow blocklist stats # block counts, top reasons
|
|
300
|
-
```
|
|
299
|
+
securenow blocklist # list blocked IPs
|
|
300
|
+
securenow blocklist list
|
|
301
|
+
securenow blocklist add <ip> --app <key> --env production --reason "Brute force"
|
|
302
|
+
securenow blocklist remove <id>
|
|
303
|
+
securenow blocklist stats # block counts, top reasons
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Automation Rules
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
securenow automation # list blocklist automation rules
|
|
310
|
+
securenow automation show <id>
|
|
311
|
+
securenow automation dry-run <id> --limit 500
|
|
312
|
+
securenow automation execute <id> --yes
|
|
313
|
+
```
|
|
301
314
|
|
|
302
315
|
### Allowlist — Restrict to Known IPs
|
|
303
316
|
|
|
304
317
|
```bash
|
|
305
|
-
securenow allowlist # list allowed IPs
|
|
306
|
-
securenow allowlist list
|
|
307
|
-
securenow allowlist add <ip>
|
|
318
|
+
securenow allowlist # list allowed IPs
|
|
319
|
+
securenow allowlist list
|
|
320
|
+
securenow allowlist add <ip> --app <key> --env local --label "Office" --reason "Corporate VPN"
|
|
308
321
|
securenow allowlist remove <id>
|
|
309
322
|
securenow allowlist stats
|
|
310
323
|
```
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { api, requireAuth } = require('./client');
|
|
5
|
+
const ui = require('./ui');
|
|
6
|
+
|
|
7
|
+
function parseList(value) {
|
|
8
|
+
if (!value) return [];
|
|
9
|
+
return String(value).split(',').map((item) => item.trim()).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseJson(value, label) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
ui.error(`${label} must be valid JSON: ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readBody(flags) {
|
|
22
|
+
if (flags.body) return parseJson(flags.body, '--body');
|
|
23
|
+
if (flags.file) return parseJson(fs.readFileSync(flags.file, 'utf8'), '--file');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bodyFromFlags(flags, requireName = false) {
|
|
28
|
+
const body = readBody(flags) || {};
|
|
29
|
+
|
|
30
|
+
if (flags.name) body.name = flags.name;
|
|
31
|
+
if (flags.description) body.description = flags.description;
|
|
32
|
+
if (flags.conditions) body.conditions = parseJson(flags.conditions, '--conditions');
|
|
33
|
+
if (flags.actions) body.actions = parseJson(flags.actions, '--actions');
|
|
34
|
+
if (flags.logic || flags['condition-logic']) body.conditionLogic = flags.logic || flags['condition-logic'];
|
|
35
|
+
if (flags.status) body.status = flags.status;
|
|
36
|
+
|
|
37
|
+
if (flags.app || flags.apps) {
|
|
38
|
+
body.applicationsAll = false;
|
|
39
|
+
body.applicationKeys = parseList(flags.app || flags.apps);
|
|
40
|
+
}
|
|
41
|
+
if (flags['applications-all']) {
|
|
42
|
+
body.applicationsAll = true;
|
|
43
|
+
body.applicationKeys = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (flags.env || flags.environment || flags.environments) {
|
|
47
|
+
body.environmentsAll = false;
|
|
48
|
+
body.environments = parseList(flags.env || flags.environment || flags.environments);
|
|
49
|
+
}
|
|
50
|
+
if (flags['environments-all']) {
|
|
51
|
+
body.environmentsAll = true;
|
|
52
|
+
body.environments = [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (requireName && !body.name) {
|
|
56
|
+
ui.error('Rule name required. Pass --name or --body JSON.');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatApplications(rule) {
|
|
64
|
+
if (rule.applicationsAll !== false) return ui.c.cyan('all apps');
|
|
65
|
+
return (rule.applications || []).join(', ') || ui.c.dim('-');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatEnvironments(rule) {
|
|
69
|
+
if (rule.environmentsAll !== false) return ui.c.cyan('all envs');
|
|
70
|
+
return (rule.environments || []).join(', ') || ui.c.dim('-');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function list(args, flags) {
|
|
74
|
+
requireAuth();
|
|
75
|
+
const s = ui.spinner('Fetching automation rules');
|
|
76
|
+
try {
|
|
77
|
+
const data = await api.get('/automation-rules');
|
|
78
|
+
const rules = data.rules || [];
|
|
79
|
+
s.stop(`Found ${rules.length} automation rule${rules.length === 1 ? '' : 's'}`);
|
|
80
|
+
|
|
81
|
+
if (flags.json) { ui.json(data); return; }
|
|
82
|
+
|
|
83
|
+
console.log('');
|
|
84
|
+
const rows = rules.map((rule) => [
|
|
85
|
+
ui.c.dim(ui.truncate(rule._id || rule.id, 12)),
|
|
86
|
+
rule.name || '-',
|
|
87
|
+
ui.statusBadge(rule.status || 'active'),
|
|
88
|
+
formatApplications(rule),
|
|
89
|
+
formatEnvironments(rule),
|
|
90
|
+
String(rule.stats?.totalMatches ?? 0),
|
|
91
|
+
rule.stats?.lastExecutedAt ? ui.timeAgo(rule.stats.lastExecutedAt) : '-',
|
|
92
|
+
]);
|
|
93
|
+
ui.table(['ID', 'Name', 'Status', 'Apps', 'Envs', 'Matches', 'Last Run'], rows);
|
|
94
|
+
console.log('');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
s.fail('Failed to fetch automation rules');
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function show(args, flags) {
|
|
102
|
+
requireAuth();
|
|
103
|
+
const id = args[0];
|
|
104
|
+
if (!id) {
|
|
105
|
+
ui.error('Usage: securenow automation show <rule-id>');
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const s = ui.spinner('Fetching automation rule');
|
|
110
|
+
try {
|
|
111
|
+
const data = await api.get(`/automation-rules/${encodeURIComponent(id)}`);
|
|
112
|
+
const rule = data.rule || data;
|
|
113
|
+
s.stop('Automation rule loaded');
|
|
114
|
+
|
|
115
|
+
if (flags.json) { ui.json(data); return; }
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
ui.heading(rule.name || 'Automation rule');
|
|
119
|
+
ui.keyValue([
|
|
120
|
+
['ID', rule._id || rule.id || id],
|
|
121
|
+
['Status', rule.status || '-'],
|
|
122
|
+
['Applications', formatApplications(rule)],
|
|
123
|
+
['Environments', formatEnvironments(rule)],
|
|
124
|
+
['Condition logic', rule.conditionLogic || 'AND'],
|
|
125
|
+
['Conditions', JSON.stringify(rule.conditions || [])],
|
|
126
|
+
['Actions', JSON.stringify(rule.actions || [])],
|
|
127
|
+
['Total executions', String(rule.stats?.totalExecutions ?? 0)],
|
|
128
|
+
['Total matches', String(rule.stats?.totalMatches ?? 0)],
|
|
129
|
+
]);
|
|
130
|
+
console.log('');
|
|
131
|
+
} catch (err) {
|
|
132
|
+
s.fail('Failed to fetch automation rule');
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function create(args, flags) {
|
|
138
|
+
requireAuth();
|
|
139
|
+
const body = bodyFromFlags(flags, true);
|
|
140
|
+
const s = ui.spinner('Creating automation rule');
|
|
141
|
+
try {
|
|
142
|
+
const data = await api.post('/automation-rules', body);
|
|
143
|
+
s.stop('Automation rule created');
|
|
144
|
+
if (flags.json) { ui.json(data); return; }
|
|
145
|
+
ui.success(`${data.rule?.name || body.name} created`);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
s.fail('Failed to create automation rule');
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function update(args, flags) {
|
|
153
|
+
requireAuth();
|
|
154
|
+
const id = args[0];
|
|
155
|
+
if (!id) {
|
|
156
|
+
ui.error('Usage: securenow automation update <rule-id> [--body JSON]');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const body = bodyFromFlags(flags, false);
|
|
161
|
+
if (Object.keys(body).length === 0) {
|
|
162
|
+
ui.error('Nothing to update. Pass --body, --file, or field flags.');
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const s = ui.spinner('Updating automation rule');
|
|
167
|
+
try {
|
|
168
|
+
const data = await api.put(`/automation-rules/${encodeURIComponent(id)}`, body);
|
|
169
|
+
s.stop('Automation rule updated');
|
|
170
|
+
if (flags.json) { ui.json(data); return; }
|
|
171
|
+
ui.success('Automation rule updated');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
s.fail('Failed to update automation rule');
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function dryRun(args, flags) {
|
|
179
|
+
requireAuth();
|
|
180
|
+
const id = args[0];
|
|
181
|
+
if (!id) {
|
|
182
|
+
ui.error('Usage: securenow automation dry-run <rule-id> [--limit 500] [--sample-limit 20]');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const body = {};
|
|
187
|
+
if (flags.limit) body.limit = Number(flags.limit);
|
|
188
|
+
if (flags['sample-limit']) body.sampleLimit = Number(flags['sample-limit']);
|
|
189
|
+
|
|
190
|
+
const s = ui.spinner('Dry-running automation rule');
|
|
191
|
+
try {
|
|
192
|
+
const data = await api.post(`/automation-rules/${encodeURIComponent(id)}/dry-run`, body);
|
|
193
|
+
s.stop('Dry-run complete');
|
|
194
|
+
if (flags.json) { ui.json(data); return; }
|
|
195
|
+
|
|
196
|
+
const results = data.results || {};
|
|
197
|
+
console.log('');
|
|
198
|
+
ui.keyValue([
|
|
199
|
+
['Scanned IPs', String(results.scanned ?? 0)],
|
|
200
|
+
['Matched IPs', String(results.matched ?? 0)],
|
|
201
|
+
['Sample count', String((results.samples || []).length)],
|
|
202
|
+
]);
|
|
203
|
+
if ((results.samples || []).length) {
|
|
204
|
+
console.log('');
|
|
205
|
+
const rows = results.samples.map((sample) => [
|
|
206
|
+
sample.ip,
|
|
207
|
+
ui.truncate((sample.path || []).join(', '), 32),
|
|
208
|
+
ui.truncate((sample.attackType || []).join(', '), 28),
|
|
209
|
+
(sample.environment || []).join(', ') || '-',
|
|
210
|
+
String(sample.riskScore ?? '-'),
|
|
211
|
+
]);
|
|
212
|
+
ui.table(['IP', 'Paths', 'Attack', 'Env', 'Risk'], rows);
|
|
213
|
+
}
|
|
214
|
+
console.log('');
|
|
215
|
+
} catch (err) {
|
|
216
|
+
s.fail('Failed to dry-run automation rule');
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function execute(args, flags) {
|
|
222
|
+
requireAuth();
|
|
223
|
+
const id = args[0];
|
|
224
|
+
if (!id) {
|
|
225
|
+
ui.error('Usage: securenow automation execute <rule-id> --yes');
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
if (!flags.yes && !flags.force) {
|
|
229
|
+
const ok = await ui.confirm('Execute this automation rule now? It may add IPs to the blocklist.');
|
|
230
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const s = ui.spinner('Executing automation rule');
|
|
234
|
+
try {
|
|
235
|
+
const data = await api.post(`/automation-rules/${encodeURIComponent(id)}/execute`, {});
|
|
236
|
+
s.stop('Automation rule executed');
|
|
237
|
+
if (flags.json) { ui.json(data); return; }
|
|
238
|
+
const r = data.results || {};
|
|
239
|
+
ui.keyValue([
|
|
240
|
+
['Scanned', String(r.scanned ?? 0)],
|
|
241
|
+
['Matched', String(r.matched ?? 0)],
|
|
242
|
+
['Blocked', String(r.blocked ?? 0)],
|
|
243
|
+
['Skipped', String(r.skipped ?? 0)],
|
|
244
|
+
['Errors', String(r.errors ?? 0)],
|
|
245
|
+
]);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
s.fail('Failed to execute automation rule');
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function remove(args, flags) {
|
|
253
|
+
requireAuth();
|
|
254
|
+
const id = args[0];
|
|
255
|
+
if (!id) {
|
|
256
|
+
ui.error('Usage: securenow automation delete <rule-id> --yes');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
if (!flags.yes && !flags.force) {
|
|
260
|
+
const ok = await ui.confirm('Delete this automation rule?');
|
|
261
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const s = ui.spinner('Deleting automation rule');
|
|
265
|
+
try {
|
|
266
|
+
const data = await api.delete(`/automation-rules/${encodeURIComponent(id)}`);
|
|
267
|
+
s.stop('Automation rule deleted');
|
|
268
|
+
if (flags.json) ui.json(data);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
s.fail('Failed to delete automation rule');
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { list, show, create, update, dryRun, execute, remove };
|
package/cli/firewall.js
CHANGED
|
@@ -7,13 +7,16 @@ function resolveEnvironment(flags) {
|
|
|
7
7
|
return flags.env || flags.environment || 'production';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
async function status(args, flags) {
|
|
11
|
-
requireAuth();
|
|
12
|
-
const s = ui.spinner('Checking firewall status');
|
|
13
|
-
|
|
14
|
-
try {
|
|
10
|
+
async function status(args, flags) {
|
|
11
|
+
requireAuth();
|
|
12
|
+
const s = ui.spinner('Checking firewall status');
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
15
|
const environment = resolveEnvironment(flags);
|
|
16
|
-
const
|
|
16
|
+
const appKey = await resolveAppKey(flags);
|
|
17
|
+
const query = { environment };
|
|
18
|
+
if (appKey) query.appKey = appKey;
|
|
19
|
+
const data = await api.get('/firewall/status', { query });
|
|
17
20
|
|
|
18
21
|
s.stop('Firewall status retrieved');
|
|
19
22
|
|
|
@@ -23,10 +26,14 @@ async function status(args, flags) {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
console.log('');
|
|
26
|
-
|
|
29
|
+
const enabledLabel = data.firewallEnabled === false
|
|
30
|
+
? ui.c.bold(ui.c.yellow('Firewall: DISABLED FOR APP/ENV'))
|
|
31
|
+
: ui.c.bold(ui.c.green('Firewall: ENABLED'));
|
|
32
|
+
console.log(` ${enabledLabel}`);
|
|
27
33
|
console.log('');
|
|
28
34
|
ui.keyValue([
|
|
29
35
|
['Blocked IPs', `${data.totalIps} total (${data.exactCount} exact + ${data.cidrCount} CIDR ranges)`],
|
|
36
|
+
['App scope', appKey || 'all apps'],
|
|
30
37
|
['Environment', data.environment || environment],
|
|
31
38
|
['Last updated', data.updatedAt || 'unknown'],
|
|
32
39
|
['Allowed IPs', data.allowlistCount != null ? `${data.allowlistCount} total (${data.allowlistExactCount} exact + ${data.allowlistCidrCount} CIDR ranges)` : '0'],
|
|
@@ -66,7 +73,10 @@ async function testIp(args, flags) {
|
|
|
66
73
|
|
|
67
74
|
try {
|
|
68
75
|
const environment = resolveEnvironment(flags);
|
|
69
|
-
const
|
|
76
|
+
const appKey = await resolveAppKey(flags);
|
|
77
|
+
const query = { environment };
|
|
78
|
+
if (appKey) query.appKey = appKey;
|
|
79
|
+
const data = await api.get(`/firewall/check/${encodeURIComponent(ip)}`, { query });
|
|
70
80
|
|
|
71
81
|
s.stop(`IP ${ip} checked`);
|
|
72
82
|
|
|
@@ -75,8 +85,14 @@ async function testIp(args, flags) {
|
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
console.log('');
|
|
79
|
-
if (data.
|
|
88
|
+
console.log('');
|
|
89
|
+
if (data.firewallEnabled === false) {
|
|
90
|
+
console.log(` ${ui.c.bold(ui.c.yellow('NOT ENFORCED'))} — firewall is disabled for ${appKey || 'this app'} (${data.environment || environment})`);
|
|
91
|
+
if (data.reason) console.log(` ${ui.c.dim(`Reason: ${data.reason}`)}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (data.blocked) {
|
|
80
96
|
if (data.allowlistActive && !data.allowlisted) {
|
|
81
97
|
console.log(` ${ui.c.bold(ui.c.red('BLOCKED'))} — ${ip} is not on the allowlist`);
|
|
82
98
|
console.log(` ${ui.c.dim('Allowlist is active — only listed IPs are permitted')}`);
|
|
@@ -92,8 +108,9 @@ async function testIp(args, flags) {
|
|
|
92
108
|
} else {
|
|
93
109
|
console.log(` ${ui.c.bold(ui.c.green('ALLOWED'))} — ${ip} is not in the blocklist`);
|
|
94
110
|
}
|
|
95
|
-
}
|
|
96
|
-
console.log(` ${ui.c.dim(`
|
|
111
|
+
}
|
|
112
|
+
console.log(` ${ui.c.dim(`App scope: ${appKey || 'all apps'} · Environment: ${data.environment || environment}`)}`);
|
|
113
|
+
console.log(` ${ui.c.dim(`Blocklist contains ${data.totalBlockedIps} entries`)}`);
|
|
97
114
|
if (data.allowlistActive) {
|
|
98
115
|
console.log(` ${ui.c.dim('Allowlist is active')}`);
|
|
99
116
|
}
|
package/cli/human.js
CHANGED
|
@@ -214,6 +214,40 @@ async function show(args, flags) {
|
|
|
214
214
|
const s = ui.spinner('Fetching human action detail');
|
|
215
215
|
try {
|
|
216
216
|
const { task, row } = await resolveTask(ref, flags);
|
|
217
|
+
if (task.kind === 'case_action' || task.actionKey) {
|
|
218
|
+
const [notification, agentCase] = await Promise.all([
|
|
219
|
+
api.get(`/notifications/${encodeURIComponent(task.notificationId)}`).catch(() => ({})),
|
|
220
|
+
api.get(`/notifications/${encodeURIComponent(task.notificationId)}/agent-case`).catch(() => ({})),
|
|
221
|
+
]);
|
|
222
|
+
s.stop('Case action loaded');
|
|
223
|
+
if (flags.json) {
|
|
224
|
+
ui.json({ task, notification, agentCase });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
console.log('');
|
|
228
|
+
printTaskSummary(task, row);
|
|
229
|
+
console.log('');
|
|
230
|
+
ui.subheading('Case action');
|
|
231
|
+
ui.keyValue([
|
|
232
|
+
['Action key', task.actionKey || '-'],
|
|
233
|
+
['Type', task.actionType || task.action?.type || '-'],
|
|
234
|
+
['Title', task.action?.title || task.title || '-'],
|
|
235
|
+
['Description', ui.truncate(task.action?.description || '', 140)],
|
|
236
|
+
['Status', task.action?.status || 'proposed'],
|
|
237
|
+
]);
|
|
238
|
+
if ((agentCase.proposedActions || []).length) {
|
|
239
|
+
console.log('');
|
|
240
|
+
const rows = agentCase.proposedActions.map((action) => [
|
|
241
|
+
action.actionKey || '-',
|
|
242
|
+
action.type || '-',
|
|
243
|
+
action.status || '-',
|
|
244
|
+
ui.truncate(action.title || action.description || '', 80),
|
|
245
|
+
]);
|
|
246
|
+
ui.table(['Key', 'Type', 'Status', 'Action'], rows);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
217
251
|
const [ipReport, notification] = await Promise.all([
|
|
218
252
|
api.get(`/notifications/${encodeURIComponent(task.notificationId)}/ips/${encodeURIComponent(task.ip)}`),
|
|
219
253
|
api.get(`/notifications/${encodeURIComponent(task.notificationId)}`).catch(() => ({})),
|
|
@@ -248,6 +282,63 @@ async function show(args, flags) {
|
|
|
248
282
|
}
|
|
249
283
|
}
|
|
250
284
|
|
|
285
|
+
async function action(args, flags) {
|
|
286
|
+
requireAuth();
|
|
287
|
+
const ref = args[0];
|
|
288
|
+
if (!ref) {
|
|
289
|
+
ui.error('Usage: securenow human action <row|notificationId> [actionKey] --status approved|rejected|executed|failed --yes --reason "..."');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const parsed = parseTaskRef(ref);
|
|
294
|
+
let notificationId = parsed?.notificationId;
|
|
295
|
+
let actionKey = flags.actionKey || flags['action-key'] || args[1];
|
|
296
|
+
let task = null;
|
|
297
|
+
let row = null;
|
|
298
|
+
|
|
299
|
+
if (parsed?.kind === 'row' || !actionKey) {
|
|
300
|
+
const resolved = await resolveTask(ref, flags);
|
|
301
|
+
task = resolved.task;
|
|
302
|
+
row = resolved.row;
|
|
303
|
+
notificationId = task.notificationId;
|
|
304
|
+
actionKey = actionKey || task.actionKey;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const status = flags.status || args[2];
|
|
308
|
+
if (!notificationId || !actionKey || !status) {
|
|
309
|
+
ui.error('notificationId, actionKey, and --status are required.');
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!['proposed', 'approved', 'rejected', 'executed', 'failed'].includes(status)) {
|
|
314
|
+
ui.error('Status must be one of: proposed, approved, rejected, executed, failed');
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!flags.yes && !flags.force) {
|
|
319
|
+
const label = task ? `row ${row || ''} (${actionKey})` : `${notificationId}/${actionKey}`;
|
|
320
|
+
const ok = await ui.confirm(`Set case action ${label} to ${status}?`);
|
|
321
|
+
if (!ok) { ui.info('Cancelled'); return; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = flags.result ? JSON.parse(flags.result) : {};
|
|
325
|
+
if (flags.reason) result.reason = flags.reason;
|
|
326
|
+
|
|
327
|
+
const s = ui.spinner('Updating case action');
|
|
328
|
+
try {
|
|
329
|
+
const data = await api.put(
|
|
330
|
+
`/notifications/${encodeURIComponent(notificationId)}/agent-case/actions/${encodeURIComponent(actionKey)}`,
|
|
331
|
+
{ status, result }
|
|
332
|
+
);
|
|
333
|
+
s.stop('Case action updated');
|
|
334
|
+
if (flags.json) { ui.json(data); return; }
|
|
335
|
+
ui.success(`${actionKey} -> ${data.action?.status || status}`);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
s.fail('Failed to update case action');
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
251
342
|
async function block(args, flags) {
|
|
252
343
|
requireAuth();
|
|
253
344
|
const ref = args[0];
|
|
@@ -328,10 +419,12 @@ function mcpPromptText(ref, flags = {}) {
|
|
|
328
419
|
: '2. For each task, call securenow_notifications_get and securenow_human_action_report for the notificationId and IP.',
|
|
329
420
|
'3. Read the AI report, finalDecision, DAG steps, findings, proofs, metadata paths/user agents/status codes, and trace IDs.',
|
|
330
421
|
'4. Open/inspect trace evidence with securenow_traces_show and securenow_logs_for_trace when trace IDs are available.',
|
|
331
|
-
'5. Decide one
|
|
422
|
+
'5. Decide one outcome: block the IP, mark a scoped false positive, recommend alert-rule tuning, or skip if evidence is ambiguous.',
|
|
332
423
|
'6. For block decisions, call securenow_human_action_block with confirm:true and a precise reason.',
|
|
333
424
|
'7. For false positives, call securenow_human_action_false_positive with confirm:true, the narrowest available conditions, and a precise reason.',
|
|
334
|
-
'8.
|
|
425
|
+
'8. For case-level tune_rule/create_exclusion rows, inspect the notification case and use securenow_human_case_action_update only when the proposed action is safe.',
|
|
426
|
+
'9. If many IPs share the same benign path/status/user-agent pattern, recommend tightening the alert rule with a precise guard instead of reviewing each IP as malicious.',
|
|
427
|
+
'10. Summarize each row handled, skipped, rule tuning needed, and still waiting. Do not globally trust an IP by default.',
|
|
335
428
|
'',
|
|
336
429
|
'Safety:',
|
|
337
430
|
'- Do not call write tools without confirm:true and a reason.',
|
|
@@ -359,6 +452,7 @@ async function work(args, flags) {
|
|
|
359
452
|
module.exports = {
|
|
360
453
|
list,
|
|
361
454
|
show,
|
|
455
|
+
action,
|
|
362
456
|
block,
|
|
363
457
|
fp,
|
|
364
458
|
prompt,
|