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/fp.js DELETED
@@ -1,638 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { api, requireAuth } = require('./client');
6
- const ui = require('./ui');
7
-
8
- // ── Helpers ──
9
-
10
- function parseConditions(flags) {
11
- if (!flags.conditions) return null;
12
- try {
13
- const parsed = JSON.parse(flags.conditions);
14
- if (!Array.isArray(parsed)) throw new Error();
15
- return parsed;
16
- } catch {
17
- ui.error('--conditions must be a valid JSON array, e.g. \'[{"field":"path","operator":"equals","value":"/api/event"}]\'');
18
- process.exit(1);
19
- }
20
- }
21
-
22
- function readBody(args, flags) {
23
- const raw = args[0] || flags.body;
24
- if (!raw && !process.stdin.isTTY) {
25
- try {
26
- return fs.readFileSync(0, 'utf8');
27
- } catch { return null; }
28
- }
29
- if (!raw) return null;
30
- if (raw.startsWith('@')) {
31
- const filePath = path.resolve(raw.slice(1));
32
- if (!fs.existsSync(filePath)) {
33
- ui.error(`File not found: ${filePath}`);
34
- process.exit(1);
35
- }
36
- return fs.readFileSync(filePath, 'utf8');
37
- }
38
- return raw;
39
- }
40
-
41
- function conditionsSummary(conditions, matchMode) {
42
- if (!conditions || !conditions.length) return ui.c.dim('(none)');
43
- const mode = matchMode === 'any' ? ' OR ' : ' AND ';
44
- return conditions.map(c => `${c.field} ${c.operator} "${ui.truncate(c.value || '', 30)}"`).join(mode);
45
- }
46
-
47
- function printConditions(conditions, matchMode) {
48
- if (!conditions?.length) { console.log(ui.c.dim(' No conditions')); return; }
49
- const modeLabel = matchMode === 'any' ? ui.c.yellow('ANY (OR)') : ui.c.cyan('ALL (AND)');
50
- console.log(` Match mode: ${modeLabel}\n`);
51
- conditions.forEach((c, i) => {
52
- const prefix = i > 0 ? ` ${matchMode === 'any' ? 'OR ' : 'AND'}` : ' ';
53
- console.log(`${prefix} ${ui.c.bold(c.field)} ${ui.c.cyan(c.operator)} ${ui.c.dim('"')}${c.value || ''}${ui.c.dim('"')}`);
54
- });
55
- }
56
-
57
- // ── List ──
58
-
59
- async function list(args, flags) {
60
- requireAuth();
61
- const s = ui.spinner('Fetching exclusion rules');
62
- try {
63
- const data = await api.get('/false-positives');
64
- const exclusions = data.exclusions || [];
65
- s.stop(`Found ${exclusions.length} exclusion rule${exclusions.length !== 1 ? 's' : ''}`);
66
-
67
- if (flags.json) { ui.json(data); return; }
68
-
69
- console.log('');
70
- if (!exclusions.length) {
71
- ui.info('No exclusion rules found. Create one with: securenow fp create');
72
- console.log('');
73
- return;
74
- }
75
-
76
- const rows = exclusions.map(e => {
77
- const scope = e.type === 'global' ? ui.c.cyan('All Rules') : `Rule: ${ui.truncate(e.ruleName || '?', 18)}`;
78
- return [
79
- ui.c.dim(ui.truncate(e._id, 12)),
80
- scope,
81
- ui.truncate(conditionsSummary(e.conditions, e.matchMode), 40),
82
- (e.matchMode || 'all').toUpperCase(),
83
- ui.statusBadge(e.isActive !== false ? 'enabled' : 'disabled'),
84
- ui.truncate(e.reason || '', 20),
85
- ui.timeAgo(e.createdAt),
86
- ];
87
- });
88
- ui.table(['ID', 'Scope', 'Conditions', 'Mode', 'Status', 'Reason', 'Created'], rows);
89
- console.log('');
90
- } catch (err) {
91
- s.fail('Failed to fetch exclusion rules');
92
- throw err;
93
- }
94
- }
95
-
96
- // ── Show ──
97
-
98
- async function show(args, flags) {
99
- requireAuth();
100
- const id = args[0];
101
- if (!id) {
102
- ui.error('Exclusion ID required. Usage: securenow fp show <id>');
103
- process.exit(1);
104
- }
105
-
106
- const s = ui.spinner('Fetching exclusion details');
107
- try {
108
- const [data, countData] = await Promise.all([
109
- api.get(`/false-positives/exclusions/${id}`),
110
- api.get(`/false-positives/result-count/${id}`).catch(() => null),
111
- ]);
112
-
113
- const excl = data.exclusion;
114
- if (!excl) {
115
- s.fail(`Exclusion ${id} not found`);
116
- process.exit(1);
117
- }
118
-
119
- s.stop('Exclusion loaded');
120
-
121
- if (flags.json) { ui.json({ exclusion: excl, scope: data.scope, resultCount: countData }); return; }
122
-
123
- console.log('');
124
- ui.heading(`Exclusion Rule: ${excl._id}`);
125
- console.log('');
126
- ui.keyValue([
127
- ['ID', excl._id],
128
- ['Active', excl.isActive !== false ? ui.c.green('Yes') : ui.c.red('No')],
129
- ['Match Mode', (excl.matchMode || 'all').toUpperCase()],
130
- ['Reason', excl.reason || ui.c.dim('—')],
131
- ['Scope', data.scope === 'rule' ? `Rule: ${excl._ruleName || excl._ruleId}` : 'Global'],
132
- ['Created', excl.createdAt ? new Date(excl.createdAt).toLocaleString() : '—'],
133
- ]);
134
-
135
- if (countData) {
136
- console.log('');
137
- ui.keyValue([
138
- ['Matched IPs', String(countData.matchCount ?? '—')],
139
- ]);
140
- }
141
-
142
- console.log('');
143
- ui.subheading('Conditions');
144
- console.log('');
145
- printConditions(excl.conditions, excl.matchMode);
146
-
147
- if (excl.pathPattern) {
148
- console.log(`\n ${ui.c.bold('Path Pattern')}: ${excl.pathPattern}`);
149
- }
150
- console.log('');
151
- } catch (err) {
152
- if (err.statusCode === 404 || err.status === 404) {
153
- s.fail(`Exclusion ${id} not found`);
154
- process.exit(1);
155
- }
156
- s.fail('Failed to fetch exclusion');
157
- throw err;
158
- }
159
- }
160
-
161
- // ── Create ──
162
-
163
- async function create(args, flags) {
164
- requireAuth();
165
- let conditions = parseConditions(flags);
166
-
167
- if (!conditions) {
168
- conditions = [];
169
- if (flags.path) {
170
- conditions.push({ field: 'path', operator: flags['path-op'] || 'equals', value: flags.path });
171
- }
172
- if (flags.method) {
173
- conditions.push({ field: 'method', operator: 'equals', value: flags.method.toUpperCase() });
174
- }
175
- if (flags['body-operator'] && flags['body-value']) {
176
- conditions.push({ field: 'request_body', operator: flags['body-operator'], value: flags['body-value'] });
177
- }
178
- if (flags['path-safe']) {
179
- conditions.push({ field: 'path', operator: 'path_safe_values', value: flags['path-safe'] === true ? 'standard' : flags['path-safe'] });
180
- }
181
- if (flags['query-safe']) {
182
- conditions.push({ field: 'path', operator: 'query_safe_values', value: flags['query-safe'] === true ? 'standard' : flags['query-safe'] });
183
- }
184
- if (flags['query-keys']) {
185
- conditions.push({ field: 'path', operator: 'query_has_only_keys', value: flags['query-keys'] });
186
- }
187
- if (flags['ua-safe']) {
188
- conditions.push({ field: 'user_agent', operator: 'ua_safe_values', value: flags['ua-safe'] === true ? 'standard' : flags['ua-safe'] });
189
- }
190
- if (flags['headers-safe']) {
191
- conditions.push({ field: 'request_headers', operator: 'headers_safe_values', value: flags['headers-safe'] === true ? 'standard' : flags['headers-safe'] });
192
- }
193
- if (flags['headers-keys']) {
194
- conditions.push({ field: 'request_headers', operator: 'headers_has_only_keys', value: flags['headers-keys'] });
195
- }
196
- if (!conditions.length) {
197
- ui.error('At least one condition is required.');
198
- ui.info('Use --conditions \'[...]\' or --path, --method, --body-operator, --path-safe, --ua-safe, --headers-safe, etc.');
199
- process.exit(1);
200
- }
201
- }
202
-
203
- const body = {
204
- conditions,
205
- matchMode: flags['match-mode'] || 'all',
206
- };
207
- if (flags.reason) body.reason = flags.reason;
208
-
209
- const scopeParams = parseRuleScope(flags);
210
- const effectiveScope = scopeParams.ruleScope || 'any_rule';
211
-
212
- if (effectiveScope === 'this_rule') {
213
- ui.error('--rule-scope this_rule requires a notification context. Use "securenow fp mark" or --rule-scope specific_rules with --target-rules.');
214
- process.exit(1);
215
- }
216
-
217
- body.ruleScope = effectiveScope;
218
- if (scopeParams.targetRuleIds) body.targetRuleIds = scopeParams.targetRuleIds;
219
-
220
- const s = ui.spinner('Creating exclusion rule');
221
- try {
222
- const data = await api.post('/false-positives/exclusions', body);
223
-
224
- s.stop('Exclusion rule created');
225
-
226
- if (flags.json) { ui.json(data); return; }
227
-
228
- console.log('');
229
- if (effectiveScope === 'any_rule') {
230
- ui.success(`Created global exclusion: ${data.exclusion?._id || '(unknown ID)'}`);
231
- } else {
232
- const ruleCount = data.rules?.length || 0;
233
- ui.success(`Created exclusion on ${ruleCount} rule${ruleCount !== 1 ? 's' : ''}`);
234
- if (data.rules) {
235
- data.rules.forEach(r => {
236
- console.log(` ${ui.c.dim('•')} ${r.ruleName || r.ruleId}`);
237
- });
238
- }
239
- }
240
- console.log('');
241
- ui.keyValue([['Scope', scopeLabel(effectiveScope)]]);
242
- console.log('');
243
- printConditions(conditions, body.matchMode);
244
- if (flags.reason) console.log(`\n Reason: ${flags.reason}`);
245
- console.log('');
246
- } catch (err) {
247
- s.fail('Failed to create exclusion rule');
248
- throw err;
249
- }
250
- }
251
-
252
- // ── Edit ──
253
-
254
- async function edit(args, flags) {
255
- requireAuth();
256
- const id = args[0];
257
- if (!id) {
258
- ui.error('Exclusion ID required. Usage: securenow fp edit <id> [--active true/false] [--reason "..."]');
259
- process.exit(1);
260
- }
261
-
262
- const body = {};
263
- if (flags.active != null) body.isActive = flags.active === 'true' || flags.active === true;
264
- if (flags.reason) body.reason = flags.reason;
265
- if (flags['match-mode']) body.matchMode = flags['match-mode'];
266
- const conditions = parseConditions(flags);
267
- if (conditions) body.conditions = conditions;
268
-
269
- if (!Object.keys(body).length) {
270
- ui.error('Nothing to update. Use --active, --reason, --match-mode, or --conditions.');
271
- process.exit(1);
272
- }
273
-
274
- const s = ui.spinner(`Updating exclusion ${id}`);
275
- try {
276
- const data = await api.put(`/false-positives/exclusions/${id}`, body);
277
- s.stop('Exclusion updated');
278
-
279
- if (flags.json) { ui.json(data); return; }
280
-
281
- console.log('');
282
- ui.success(`Updated exclusion: ${id}`);
283
- if (data.exclusion) {
284
- ui.keyValue([
285
- ['Active', data.exclusion.isActive !== false ? ui.c.green('Yes') : ui.c.red('No')],
286
- ['Match Mode', (data.exclusion.matchMode || 'all').toUpperCase()],
287
- ['Reason', data.exclusion.reason || ui.c.dim('—')],
288
- ]);
289
- }
290
- console.log('');
291
- } catch (err) {
292
- s.fail('Failed to update exclusion');
293
- throw err;
294
- }
295
- }
296
-
297
- // ── Delete ──
298
-
299
- async function remove(args, flags) {
300
- requireAuth();
301
- const id = args[0];
302
- if (!id) {
303
- ui.error('Exclusion ID required. Usage: securenow fp delete <id>');
304
- process.exit(1);
305
- }
306
-
307
- if (!flags.force && !flags.yes) {
308
- const ok = await ui.confirm(`Delete exclusion ${id}?`);
309
- if (!ok) { ui.info('Cancelled'); return; }
310
- }
311
-
312
- const s = ui.spinner(`Deleting exclusion ${id}`);
313
- try {
314
- await api.delete(`/false-positives/exclusions/${id}`);
315
- s.stop('Exclusion deleted');
316
- } catch (err) {
317
- s.fail('Failed to delete exclusion');
318
- throw err;
319
- }
320
- }
321
-
322
- // ── Test Body ──
323
-
324
- async function testBody(args, flags) {
325
- requireAuth();
326
- const bodyStr = readBody(args, flags);
327
- if (!bodyStr) {
328
- ui.error('Request body is required. Pass as argument, --body, @filepath, or pipe via stdin.');
329
- ui.info('Example: securenow fp test-body \'{"key":"value"}\' --conditions \'[...]\'');
330
- process.exit(1);
331
- }
332
-
333
- const conditions = parseConditions(flags);
334
- if (!conditions || !conditions.length) {
335
- ui.error('--conditions is required with at least one request_body condition.');
336
- process.exit(1);
337
- }
338
-
339
- const s = ui.spinner('Testing body against conditions');
340
- try {
341
- const data = await api.post('/false-positives/test-body', { body: bodyStr, conditions });
342
- s.stop('Test complete');
343
-
344
- if (flags.json) { ui.json(data); return; }
345
-
346
- console.log('');
347
- const verdict = data.allPassed
348
- ? ui.c.green('✓ ALL PASSED — this body would be excluded (false positive)')
349
- : ui.c.red('✗ FAILED — this body would still trigger alerts');
350
- console.log(` ${verdict}`);
351
- console.log(` Detected format: ${ui.c.cyan(data.detectedFormat || 'unknown')}`);
352
- console.log('');
353
-
354
- if (data.results) {
355
- const rows = data.results.map(r => {
356
- const status = r.passed ? ui.c.green('✓ pass') : ui.c.red('✗ fail');
357
- const detail = r.details?.error
358
- || (r.details?.threats?.length ? `threats: ${r.details.threats.map(t => t.attacks?.join(',')).join('; ')}` : '')
359
- || (r.details?.extraKeys?.length ? `extra keys: ${r.details.extraKeys.join(', ')}` : '')
360
- || '';
361
- return [
362
- status,
363
- r.condition?.operator || '—',
364
- ui.truncate(r.condition?.value || '', 40),
365
- ui.truncate(detail, 40),
366
- ];
367
- });
368
- ui.table(['Result', 'Operator', 'Value', 'Details'], rows);
369
- }
370
- console.log('');
371
-
372
- if (!data.allPassed) process.exit(2);
373
- } catch (err) {
374
- s.fail('Body test failed');
375
- throw err;
376
- }
377
- }
378
-
379
- // ── Dry Run ──
380
-
381
- async function dryRun(args, flags) {
382
- requireAuth();
383
- const conditions = parseConditions(flags);
384
- if (!conditions || !conditions.length) {
385
- ui.error('--conditions is required.');
386
- ui.info('Example: securenow fp dry-run --conditions \'[{"field":"path","operator":"equals","value":"/api/event"},{"field":"method","operator":"equals","value":"POST"}]\'');
387
- process.exit(1);
388
- }
389
-
390
- const body = { conditions, matchMode: flags['match-mode'] || 'all' };
391
- const s = ui.spinner('Running dry-run against live traces (last 3 days)');
392
- try {
393
- const data = await api.post('/false-positives/dry-run', body);
394
- s.stop('Dry-run complete');
395
-
396
- if (flags.json) { ui.json(data); return; }
397
-
398
- const total = data.totalTraces || 0;
399
- const matched = data.matchedCount || 0;
400
- const unmatched = data.unmatchedCount || 0;
401
- const pct = total > 0 ? Math.round((matched / total) * 100) : 0;
402
-
403
- console.log('');
404
- ui.heading('Dry-Run Results');
405
- console.log('');
406
- ui.keyValue([
407
- ['Total Traces', String(total)],
408
- ['Would Be Excluded', `${ui.c.green(String(matched))} (${pct}%)`],
409
- ['Would Still Alert', `${unmatched > 0 ? ui.c.red(String(unmatched)) : ui.c.green('0')} (${100 - pct}%)`],
410
- ]);
411
-
412
- if (data.queryLimitHit) {
413
- console.log(`\n ${ui.c.yellow('!')} Query limit reached (5,000 spans). Results are a sample.`);
414
- }
415
-
416
- if (data.sqlPreFiltered?.length) {
417
- console.log(`\n ${ui.c.dim('Pre-filtered in DB:')} ${data.sqlPreFiltered.join(' + ')}`);
418
- }
419
-
420
- // Aggregated body keys
421
- if (data.aggregatedBodyKeys?.length) {
422
- const keyCond = conditions.find(c =>
423
- c.field === 'request_body' && (c.operator === 'json_has_only_keys' || c.operator === 'body_has_only_fields')
424
- );
425
- if (keyCond) {
426
- const expected = new Set((keyCond.value || '').split(',').map(k => k.trim()).filter(Boolean));
427
- const extra = data.aggregatedBodyKeys.filter(k => !expected.has(k));
428
- if (extra.length) {
429
- console.log('');
430
- ui.subheading(`Keys in bodies NOT in allowlist (${extra.length})`);
431
- console.log('');
432
- extra.forEach(k => console.log(` ${ui.c.yellow('+')} ${k}`));
433
- console.log(`\n ${ui.c.dim('Copy all keys:')} ${data.aggregatedBodyKeys.join(',')}`);
434
- }
435
- }
436
- }
437
-
438
- // Unmatched traces
439
- if (data.unmatchedTraces?.length) {
440
- console.log('');
441
- ui.subheading(`Unmatched Traces (showing ${data.unmatchedTraces.length} of ${unmatched})`);
442
- console.log('');
443
- const rows = data.unmatchedTraces.map(t => [
444
- ui.c.dim(ui.truncate(t.traceId, 16)),
445
- t.method || '—',
446
- ui.truncate(t.path || '', 30),
447
- t.statusCode || '—',
448
- ui.truncate((t.failedConditions || []).join(', '), 35),
449
- ]);
450
- ui.table(['Trace ID', 'Method', 'Path', 'Status', 'Failed Conditions'], rows);
451
- }
452
-
453
- if (total > 0 && unmatched === 0) {
454
- console.log(`\n ${ui.c.green('✓')} All ${total} traces match — they would all be excluded.`);
455
- }
456
- console.log('');
457
-
458
- if (unmatched > 0) process.exit(2);
459
- } catch (err) {
460
- s.fail('Dry-run failed');
461
- throw err;
462
- }
463
- }
464
-
465
- // ── AI Fill ──
466
-
467
- async function aiFill(args, flags) {
468
- requireAuth();
469
- const description = args.join(' ') || flags.description;
470
- let httpContext = null;
471
-
472
- if (flags.context) {
473
- try {
474
- httpContext = JSON.parse(flags.context);
475
- } catch {
476
- ui.error('--context must be valid JSON, e.g. \'{"method":"POST","url":"/api/event","requestBody":"..."}\'');
477
- process.exit(1);
478
- }
479
- }
480
-
481
- if (!description && !httpContext) {
482
- ui.error('Provide a --description or --context for AI to generate conditions.');
483
- ui.info('Example: securenow fp ai-fill --description "view_cart events from /api/event"');
484
- process.exit(1);
485
- }
486
-
487
- const body = {};
488
- if (description) body.description = description;
489
- if (httpContext) body.httpContext = httpContext;
490
-
491
- const s = ui.spinner('Generating conditions with AI');
492
- try {
493
- const data = await api.post('/false-positives/ai-fill', body);
494
- s.stop('AI suggestion ready');
495
-
496
- if (flags.json) { ui.json(data); return; }
497
-
498
- console.log('');
499
- ui.heading('AI-Generated Exclusion');
500
- console.log('');
501
-
502
- if (data.reason) {
503
- console.log(` ${ui.c.bold('Reason:')} ${data.reason}`);
504
- console.log('');
505
- }
506
-
507
- console.log(` ${ui.c.bold('Match Mode:')} ${(data.matchMode || 'all').toUpperCase()}`);
508
- console.log('');
509
-
510
- ui.subheading('Conditions');
511
- console.log('');
512
- printConditions(data.conditions, data.matchMode);
513
-
514
- console.log('');
515
- ui.subheading('Use with create');
516
- const condStr = JSON.stringify(data.conditions);
517
- console.log(`\n securenow fp create --conditions '${condStr}'${data.reason ? ` --reason "${data.reason}"` : ''}`);
518
- console.log('');
519
- } catch (err) {
520
- s.fail('AI generation failed');
521
- throw err;
522
- }
523
- }
524
-
525
- // ── Mark False Positive ──
526
-
527
- const VALID_RULE_SCOPES = ['this_rule', 'specific_rules', 'all_existing', 'any_rule'];
528
-
529
- function parseRuleScope(flags) {
530
- const scope = flags['rule-scope'];
531
- if (!scope) return {};
532
- if (!VALID_RULE_SCOPES.includes(scope)) {
533
- ui.error(`--rule-scope must be one of: ${VALID_RULE_SCOPES.join(', ')}`);
534
- process.exit(1);
535
- }
536
- const result = { ruleScope: scope };
537
- if (scope === 'specific_rules') {
538
- const raw = flags['target-rules'];
539
- if (!raw) {
540
- ui.error('--target-rules is required when --rule-scope is specific_rules (comma-separated rule IDs)');
541
- process.exit(1);
542
- }
543
- result.targetRuleIds = raw.split(',').map(id => id.trim()).filter(Boolean);
544
- if (!result.targetRuleIds.length) {
545
- ui.error('--target-rules must contain at least one rule ID');
546
- process.exit(1);
547
- }
548
- }
549
- return result;
550
- }
551
-
552
- function scopeLabel(scope) {
553
- switch (scope) {
554
- case 'this_rule': return 'This rule only';
555
- case 'specific_rules': return 'Specific rules';
556
- case 'all_existing': return 'All existing rules';
557
- case 'any_rule': return 'Any rule (incl. future)';
558
- default: return scope || 'This rule only';
559
- }
560
- }
561
-
562
- async function mark(args, flags) {
563
- requireAuth();
564
- const notifId = args[0];
565
- const ip = args[1];
566
-
567
- if (!notifId || !ip) {
568
- ui.error('Usage: securenow fp mark <notification-id> <ip> [--conditions \'[...]\'] [--reason "..."] [--rule-scope this_rule|specific_rules|all_existing|any_rule]');
569
- process.exit(1);
570
- }
571
-
572
- const body = {};
573
- const conditions = parseConditions(flags);
574
- if (conditions) body.conditions = conditions;
575
- if (flags['match-mode']) body.matchMode = flags['match-mode'];
576
- if (flags.reason) body.reason = flags.reason;
577
- body.createExclusion = flags['create-exclusion'] !== 'false' && flags['create-exclusion'] !== false;
578
- body.applyToExisting = flags['apply-existing'] !== 'false' && flags['apply-existing'] !== false;
579
-
580
- const scopeParams = parseRuleScope(flags);
581
- Object.assign(body, scopeParams);
582
-
583
- const s = ui.spinner(`Marking ${ip} as false positive`);
584
- try {
585
- const data = await api.post(`/notifications/${encodeURIComponent(notifId)}/ips/${encodeURIComponent(ip)}/false-positive`, body);
586
- s.stop('Marked as false positive');
587
-
588
- if (flags.json) { ui.json(data); return; }
589
-
590
- console.log('');
591
- ui.success(`IP ${ip} marked as false positive`);
592
- console.log('');
593
- ui.keyValue([
594
- ['Notification', notifId],
595
- ['IP', ip],
596
- ['Rule Scope', scopeLabel(data.ruleScope)],
597
- ['IPs Dismissed (same notif)', String(data.bulkCount ?? 0)],
598
- ['Cross-Notification Matches', String(data.crossNotifCount ?? 0)],
599
- ['Exclusion Rule Created', data.exclusionCreated ? ui.c.green('Yes') : ui.c.dim('No')],
600
- ]);
601
-
602
- if (data.exclusionCreated?.rules?.length > 0) {
603
- console.log('');
604
- ui.subheading(`Exclusion added to ${data.exclusionCreated.rules.length} rule${data.exclusionCreated.rules.length !== 1 ? 's' : ''}`);
605
- console.log('');
606
- data.exclusionCreated.rules.forEach(r => {
607
- console.log(` ${ui.c.dim('•')} ${r.ruleName} ${ui.c.dim(`(${r.ruleId})`)}`);
608
- });
609
- }
610
-
611
- if (data.crossNotifDetails?.length) {
612
- console.log('');
613
- ui.subheading('Affected Notifications');
614
- console.log('');
615
- const rows = data.crossNotifDetails.map(d => [
616
- ui.truncate(d.title || d.notificationId, 40),
617
- String(d.dismissedCount),
618
- ]);
619
- ui.table(['Notification', 'Dismissed'], rows);
620
- }
621
- console.log('');
622
- } catch (err) {
623
- s.fail('Failed to mark as false positive');
624
- throw err;
625
- }
626
- }
627
-
628
- module.exports = {
629
- list,
630
- show,
631
- create,
632
- edit,
633
- remove,
634
- testBody,
635
- dryRun,
636
- aiFill,
637
- mark,
638
- };