securenow 8.15.0 → 8.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 +5 -0
- package/SKILL-CLI.md +5 -0
- package/cli/monitor.js +3 -1
- package/cli/security.js +179 -6
- package/cli.js +11 -4
- package/mcp/catalog.js +64 -0
- package/package.json +1 -1
package/NPM_README.md
CHANGED
|
@@ -339,6 +339,11 @@ verification plus SQL validation. See `node_modules/securenow/TELEMETRY-SQL.md`
|
|
|
339
339
|
timestamp handling, privacy rules, templates, and the full test-to-production lifecycle.
|
|
340
340
|
|
|
341
341
|
```bash
|
|
342
|
+
# Describe a detection in English (new in 8.16): SecureNow picks an instant rule (every request,
|
|
343
|
+
# as it arrives) or a periodic one (SQL on a schedule), writes it from your app's real data,
|
|
344
|
+
# checks it against that data, and creates it in test mode when you confirm
|
|
345
|
+
npx securenow alerts rules from-text "a sign-in from a new address, then a password change within 5 minutes" --app <app-key>
|
|
346
|
+
|
|
342
347
|
# Create a custom detection rule from your own SQL (new in 8.1)
|
|
343
348
|
npx securenow alerts rules create \
|
|
344
349
|
--name "Auth: magic-link brute force" \
|
package/SKILL-CLI.md
CHANGED
|
@@ -275,6 +275,11 @@ its fields were observed and `alerts rules validate` succeeds. Otherwise label i
|
|
|
275
275
|
```bash
|
|
276
276
|
securenow alerts # list alert rules (default)
|
|
277
277
|
securenow alerts rules # list alert rules (columns: Status, Applications, Schedule)
|
|
278
|
+
securenow alerts rules from-text \ # describe a detection in English; checked draft, then create (test mode)
|
|
279
|
+
"one IP failing 10 sign-ins in 5 minutes" \
|
|
280
|
+
--app <key> [--env production] # picks instant or periodic; --instant/--periodic, --draft-only, --yes
|
|
281
|
+
securenow alerts rules create --name "Secrets probe" --apps <key> \
|
|
282
|
+
--conditions '[{"field":"path","operator":"contains","value":"/.env"}]' # an instant rule, no SQL
|
|
278
283
|
securenow alerts rules validate \ # dry-run a candidate query BEFORE creating any rule
|
|
279
284
|
--sql @rule.sql --app <key> # no rule id needed — auto-hosts on an existing (system) rule
|
|
280
285
|
securenow alerts rules create \ # create a custom detection rule from inline SQL
|
package/cli/monitor.js
CHANGED
|
@@ -342,6 +342,8 @@ async function notificationsList(args, flags) {
|
|
|
342
342
|
const s = ui.spinner('Fetching notifications');
|
|
343
343
|
try {
|
|
344
344
|
const query = { limit: flags.limit || 20, page: flags.page || 1 };
|
|
345
|
+
if (flags.search) query.search = String(flags.search);
|
|
346
|
+
if (flags.severity) query.severity = String(flags.severity).toLowerCase();
|
|
345
347
|
const data = await api.get('/notifications', { query });
|
|
346
348
|
const notifications = data.notifications || [];
|
|
347
349
|
const pagination = data.pagination;
|
|
@@ -354,7 +356,7 @@ async function notificationsList(args, flags) {
|
|
|
354
356
|
ui.c.dim(n.id || n._id || ''),
|
|
355
357
|
ui.statusBadge(n.read ? 'read' : 'unread'),
|
|
356
358
|
ui.truncate(n.title || n.message || n.type || '', 50),
|
|
357
|
-
n.ip || '—',
|
|
359
|
+
n.primaryIp || n.ip || '—',
|
|
358
360
|
n.severity ? ui.statusBadge(n.severity) : '—',
|
|
359
361
|
ui.timeAgo(n.createdAt),
|
|
360
362
|
]);
|
package/cli/security.js
CHANGED
|
@@ -39,6 +39,9 @@ async function alertRulesRoute(args, flags) {
|
|
|
39
39
|
if (sub === 'create' || sub === 'add') {
|
|
40
40
|
return alertRuleCreate(args.slice(1), flags);
|
|
41
41
|
}
|
|
42
|
+
if (sub === 'from-text' || sub === 'draft') {
|
|
43
|
+
return alertRuleFromText(args.slice(1), flags);
|
|
44
|
+
}
|
|
42
45
|
if (sub === 'show') {
|
|
43
46
|
return alertRuleShow(args.slice(1), flags);
|
|
44
47
|
}
|
|
@@ -125,8 +128,8 @@ async function alertRuleSetMode(args, flags, mode) {
|
|
|
125
128
|
|
|
126
129
|
// Duplicate any rule (including a read-only system template) into a new CUSTOM rule you
|
|
127
130
|
// own. System rules are view-only examples — fork one with this to enable / edit it. The
|
|
128
|
-
// copy defaults to Disabled (opt in with --enable / --status active)
|
|
129
|
-
//
|
|
131
|
+
// copy defaults to Disabled (opt in with --enable / --status active) and to test mode
|
|
132
|
+
// (--mode prod to arm it); SQL, instant.block, schedule and scope are copied from the source.
|
|
130
133
|
async function alertRuleDuplicate(args, flags) {
|
|
131
134
|
requireAuth();
|
|
132
135
|
const id = args[0];
|
|
@@ -221,8 +224,11 @@ async function alertRuleCreate(args, flags) {
|
|
|
221
224
|
|
|
222
225
|
const queryMappingId = flags['query-mapping-id'] || flags.queryMappingId;
|
|
223
226
|
const sqlQuery = readSqlArg(flags);
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
const instantConditions = flags.conditions ? readJsonArg(flags.conditions, '--conditions') : null;
|
|
228
|
+
if (!queryMappingId && !sqlQuery && !instantConditions) {
|
|
229
|
+
ui.error('Provide the detection query: --sql <sql|@file|-> (or reuse one with --query-mapping-id <id>),');
|
|
230
|
+
ui.error('or an instant rule: --conditions \'[{"field":"path","operator":"contains","value":"/.env"}]\' [--match any|all].');
|
|
231
|
+
ui.info('Or describe it in English: securenow alerts rules from-text "<what to catch>" --app <key>');
|
|
226
232
|
console.log(ui.c.dim(' Scope app keys in SQL with the __USER_APP_KEYS__ placeholder. The column differs by table:'));
|
|
227
233
|
console.log(ui.c.dim(" traces: WHERE `resource_string_service$$name` IN (__USER_APP_KEYS__) AND timestamp >= now64(9) - INTERVAL 15 MINUTE"));
|
|
228
234
|
console.log(ui.c.dim(" logs: WHERE resources_string['service.name'] IN (__USER_APP_KEYS__) AND timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)"));
|
|
@@ -239,12 +245,22 @@ async function alertRuleCreate(args, flags) {
|
|
|
239
245
|
|
|
240
246
|
const body = { name };
|
|
241
247
|
if (flags.description) body.description = flags.description;
|
|
242
|
-
if (
|
|
248
|
+
if (instantConditions) {
|
|
249
|
+
// An instant rule: checked against every incoming request, no SQL and no schedule.
|
|
250
|
+
body.executionMode = 'instant';
|
|
251
|
+
body.instant = {
|
|
252
|
+
enabled: true,
|
|
253
|
+
matchMode: flags.match === 'all' ? 'all' : 'any',
|
|
254
|
+
conditions: Array.isArray(instantConditions) ? instantConditions : [instantConditions],
|
|
255
|
+
};
|
|
256
|
+
const env = resolveEnvironment(flags);
|
|
257
|
+
if (env) body.instant.environments = String(env).split(',').map((e) => e.trim()).filter(Boolean);
|
|
258
|
+
} else if (queryMappingId) body.queryMappingId = queryMappingId;
|
|
243
259
|
else body.sqlQuery = sqlQuery;
|
|
244
260
|
if (flags.nlp || flags.text) body.nlpQuery = flags.nlp || flags.text;
|
|
245
261
|
if (flags.category) body.category = flags.category;
|
|
246
262
|
if (flags.severity) body.severity = String(flags.severity).toLowerCase();
|
|
247
|
-
if (flags['execution-mode']) body.executionMode = flags['execution-mode'];
|
|
263
|
+
if (flags['execution-mode'] && !instantConditions) body.executionMode = flags['execution-mode'];
|
|
248
264
|
|
|
249
265
|
if (hasAll) {
|
|
250
266
|
body.applicationsAll = true;
|
|
@@ -646,6 +662,163 @@ function readSqlArg(flags) {
|
|
|
646
662
|
return text;
|
|
647
663
|
}
|
|
648
664
|
|
|
665
|
+
// JSON from a flag value, '@file' or '-' (stdin).
|
|
666
|
+
function readJsonArg(raw, label) {
|
|
667
|
+
const text = String(raw);
|
|
668
|
+
const source = text === '-' ? fs.readFileSync(0, 'utf8') : text.startsWith('@') ? fs.readFileSync(text.slice(1), 'utf8') : text;
|
|
669
|
+
try {
|
|
670
|
+
return JSON.parse(source);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
ui.error(`${label} is not valid JSON: ${err.message}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ── English → alert rule ──
|
|
678
|
+
// The API drafts the rule from the app's real data (instant or periodic), checks it against that
|
|
679
|
+
// data, and returns the draft. Nothing is created until you confirm; new rules start in test mode.
|
|
680
|
+
|
|
681
|
+
async function waitForDraft(draftId, s) {
|
|
682
|
+
for (let i = 0; i < 90; i++) {
|
|
683
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
684
|
+
const result = await api.get(`/alert-rules/draft/${draftId}`);
|
|
685
|
+
if (result.status !== 'running') return result;
|
|
686
|
+
if (s && i === 6) s.update('Checking the draft against your data');
|
|
687
|
+
}
|
|
688
|
+
return { status: 'failed', error: 'The draft is still running after 2 minutes; try again' };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function draftCell(value) {
|
|
692
|
+
if (value == null) return '';
|
|
693
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
694
|
+
return text.length > 40 ? `${text.slice(0, 37)}…` : text;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function printDraft(result) {
|
|
698
|
+
const d = result.draft || {};
|
|
699
|
+
const check = result.check || {};
|
|
700
|
+
const kindLabel = d.kind === 'instant' ? ui.c.cyan('Instant rule') : ui.c.cyan('Periodic rule');
|
|
701
|
+
console.log('');
|
|
702
|
+
console.log(` ${kindLabel} ${ui.c.dim('·')} ${d.severity || 'medium'} ${ui.c.dim('·')} ${d.category || 'custom'}`);
|
|
703
|
+
console.log(` ${ui.c.bold(d.name || '')}`);
|
|
704
|
+
if (d.summary) console.log(` ${d.summary}`);
|
|
705
|
+
if (d.whyThisKind) console.log(ui.c.dim(` Why ${d.kind}: ${d.whyThisKind}`));
|
|
706
|
+
console.log('');
|
|
707
|
+
if (d.kind === 'instant') {
|
|
708
|
+
const conditions = d.instant?.conditions || [];
|
|
709
|
+
const envs = d.instant?.environments || [];
|
|
710
|
+
console.log(` Watches every request, as it arrives${envs.length ? `, in ${envs.join(', ')}` : ', in every environment'}`);
|
|
711
|
+
console.log(` When ${d.instant?.matchMode === 'all' ? 'all of' : 'any of'}`);
|
|
712
|
+
for (const c of conditions) console.log(` ${c.field} ${ui.c.dim(c.operator)} ${c.value}`);
|
|
713
|
+
} else {
|
|
714
|
+
const parts = [d.schedule?.description || d.schedule?.cronExpression];
|
|
715
|
+
if (d.windowMinutes) parts.push(`window ${d.windowMinutes} min`);
|
|
716
|
+
if (d.threshold) parts.push(d.threshold);
|
|
717
|
+
console.log(` Schedule ${parts.filter(Boolean).join(ui.c.dim(' · '))}`);
|
|
718
|
+
console.log(' SQL');
|
|
719
|
+
for (const line of String(d.sqlQuery || '').split('\n')) console.log(ui.c.dim(` ${line}`));
|
|
720
|
+
}
|
|
721
|
+
console.log('');
|
|
722
|
+
console.log(` Check ${check.note || ''}`);
|
|
723
|
+
const rows = check.rows || [];
|
|
724
|
+
if (rows.length) {
|
|
725
|
+
const cols = Object.keys(rows[0]).slice(0, 6);
|
|
726
|
+
console.log('');
|
|
727
|
+
ui.table(cols, rows.slice(0, 8).map((row) => cols.map((c) => draftCell(row[c]))));
|
|
728
|
+
}
|
|
729
|
+
for (const w of check.warnings || []) ui.warn(w);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function draftToCreateBody(result, flags) {
|
|
733
|
+
const d = result.draft || {};
|
|
734
|
+
const body = {
|
|
735
|
+
name: flags.name ? String(flags.name) : d.name,
|
|
736
|
+
description: d.summary || '',
|
|
737
|
+
nlpQuery: d.nlpQuery,
|
|
738
|
+
category: d.category,
|
|
739
|
+
severity: flags.severity ? String(flags.severity).toLowerCase() : d.severity,
|
|
740
|
+
applicationsAll: false,
|
|
741
|
+
applications: [result.applicationKey],
|
|
742
|
+
executionMode: d.executionMode,
|
|
743
|
+
};
|
|
744
|
+
if (d.executionMode === 'instant') body.instant = d.instant;
|
|
745
|
+
else {
|
|
746
|
+
body.sqlQuery = d.sqlQuery;
|
|
747
|
+
body.schedule = d.schedule;
|
|
748
|
+
}
|
|
749
|
+
if (flags['no-throttle']) body.throttle = { enabled: false, minutes: 0 };
|
|
750
|
+
else if (flags['throttle-minutes']) body.throttle = { enabled: true, minutes: Number(flags['throttle-minutes']) };
|
|
751
|
+
return body;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function alertRuleFromText(args, flags) {
|
|
755
|
+
requireAuth();
|
|
756
|
+
const text = (args.join(' ').trim() || (typeof flags.text === 'string' ? flags.text : '')).trim();
|
|
757
|
+
const appKey = resolveApp(flags);
|
|
758
|
+
if (!text || !appKey) {
|
|
759
|
+
ui.error('Usage: securenow alerts rules from-text "<what the rule should catch>" --app <key> [--env <environment>]');
|
|
760
|
+
console.log(ui.c.dim(' [--instant | --periodic] [--name "..."] [--severity high] [--yes] [--draft-only] [--json]'));
|
|
761
|
+
console.log(ui.c.dim(' Example: securenow alerts rules from-text "one IP failing 10 sign-ins in 5 minutes" --app <key>'));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const body = { text, applicationKey: appKey };
|
|
766
|
+
const environment = resolveEnvironment(flags);
|
|
767
|
+
if (environment) body.environment = environment;
|
|
768
|
+
if (flags.instant || flags.kind === 'instant') body.kind = 'instant';
|
|
769
|
+
else if (flags.periodic || flags.kind === 'periodic') body.kind = 'periodic';
|
|
770
|
+
|
|
771
|
+
const s = ui.spinner('Drafting the rule from your app\'s data');
|
|
772
|
+
let result;
|
|
773
|
+
try {
|
|
774
|
+
const started = await api.post('/alert-rules/draft', body);
|
|
775
|
+
result = await waitForDraft(started.draftId, s);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
s.fail('Failed to draft the rule');
|
|
778
|
+
throw err;
|
|
779
|
+
}
|
|
780
|
+
if (result.status !== 'complete') {
|
|
781
|
+
s.fail(result.error || 'Could not draft the rule');
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
s.stop('Draft ready');
|
|
785
|
+
|
|
786
|
+
if (flags.json && (flags['draft-only'] || !flags.yes)) { ui.json(result); return; }
|
|
787
|
+
printDraft(result);
|
|
788
|
+
console.log('');
|
|
789
|
+
if (flags['draft-only']) {
|
|
790
|
+
ui.info('Draft only: nothing was created. Drop --draft-only to create it.');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (!flags.yes) {
|
|
794
|
+
const ok = await ui.confirm('Create this rule? It starts in test mode: it alerts, but blocks nothing');
|
|
795
|
+
if (!ok) { ui.info('Nothing created'); return; }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const c = ui.spinner('Creating the rule');
|
|
799
|
+
let data;
|
|
800
|
+
try {
|
|
801
|
+
data = await api.post('/alert-rules', draftToCreateBody(result, flags));
|
|
802
|
+
} catch (err) {
|
|
803
|
+
c.fail('Failed to create the rule');
|
|
804
|
+
throw err;
|
|
805
|
+
}
|
|
806
|
+
const r = data.alertRule || data;
|
|
807
|
+
const id = r._id || r.id || '';
|
|
808
|
+
c.stop('Rule created');
|
|
809
|
+
if (flags.json) { ui.json({ draft: result.draft, check: result.check, alertRule: r }); return; }
|
|
810
|
+
console.log('');
|
|
811
|
+
ui.keyValue([
|
|
812
|
+
['ID', id || '-'],
|
|
813
|
+
['Name', r.name || '-'],
|
|
814
|
+
['Kind', r.executionMode === 'instant' ? 'instant (every request, as it arrives)' : `periodic (${r.schedule?.description || r.schedule?.cronExpression || '-'})`],
|
|
815
|
+
['Mode', `${r.mode || 'test'} (alerts only, no blocking)`],
|
|
816
|
+
['Application', (r.applications || []).join(', ') || '-'],
|
|
817
|
+
]);
|
|
818
|
+
console.log('');
|
|
819
|
+
ui.success(`Watching. Promote it when it has proved itself: securenow alerts rules promote ${id}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
649
822
|
async function alertRuleTest(args, flags) {
|
|
650
823
|
requireAuth();
|
|
651
824
|
const id = args[0];
|
package/cli.js
CHANGED
|
@@ -240,7 +240,7 @@ const COMMANDS = {
|
|
|
240
240
|
desc: 'Manage notifications',
|
|
241
241
|
usage: 'securenow notifications <subcommand> [options]',
|
|
242
242
|
sub: {
|
|
243
|
-
list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
|
|
243
|
+
list: { desc: 'List notifications', flags: { limit: 'Max results', page: 'Page number', search: 'Only notifications whose title, description or IP contains this text (e.g. a rule name)', severity: 'critical | high | medium | low' }, run: (a, f) => require('./cli/monitor').notificationsList(a, f) },
|
|
244
244
|
read: { desc: 'Mark notification as read', usage: 'securenow notifications read <id>', run: (a, f) => require('./cli/monitor').notificationsRead(a, f) },
|
|
245
245
|
'read-all': { desc: 'Mark all as read', run: () => require('./cli/monitor').notificationsReadAll() },
|
|
246
246
|
unread: { desc: 'Show unread count', run: () => require('./cli/monitor').notificationsUnread() },
|
|
@@ -311,11 +311,18 @@ const COMMANDS = {
|
|
|
311
311
|
usage: 'securenow alerts <subcommand> [options]',
|
|
312
312
|
sub: {
|
|
313
313
|
rules: {
|
|
314
|
-
desc: 'Create, list, show, update, duplicate, promote/demote, set-sql, validate, delete, test, or tune alert rules',
|
|
315
|
-
usage: 'securenow alerts rules <list|create|show|update|duplicate|promote|demote|set-sql|validate|delete|test|dry-run-query|tune-query|exclusions> [options]'
|
|
314
|
+
desc: 'Create (from English or SQL), list, show, update, duplicate, promote/demote, set-sql, validate, delete, test, or tune alert rules',
|
|
315
|
+
usage: 'securenow alerts rules <list|from-text|create|show|update|duplicate|promote|demote|set-sql|validate|delete|test|dry-run-query|tune-query|exclusions|versions|version|rollback> [options]\n'
|
|
316
|
+
+ ' securenow alerts rules from-text "<what to catch, in English>" --app <key> [--env <environment>] [--instant|--periodic] [--yes|--draft-only]',
|
|
316
317
|
flags: {
|
|
317
318
|
json: 'Output as JSON',
|
|
318
|
-
|
|
319
|
+
env: 'With from-text or create --conditions: only watch this environment (default: every environment)',
|
|
320
|
+
instant: 'With from-text: ask for an instant rule (every request, as it arrives)',
|
|
321
|
+
periodic: 'With from-text: ask for a periodic rule (SQL on a schedule)',
|
|
322
|
+
'draft-only': 'With from-text: show the checked draft, create nothing',
|
|
323
|
+
conditions: 'With create: instant-rule conditions as JSON, @file or - ([{"field":"path","operator":"contains","value":"/.env"}])',
|
|
324
|
+
match: 'With create --conditions: any (default) | all',
|
|
325
|
+
name: 'With create/from-text: rule name',
|
|
319
326
|
description: 'With create: rule description',
|
|
320
327
|
nlp: 'With create: plain-English intent stored on the query',
|
|
321
328
|
text: 'Alias for --nlp',
|
package/mcp/catalog.js
CHANGED
|
@@ -1378,6 +1378,70 @@ const TOOLS = [
|
|
|
1378
1378
|
...confirmSchema,
|
|
1379
1379
|
}, ['text', 'confirm', 'reason']),
|
|
1380
1380
|
},
|
|
1381
|
+
{
|
|
1382
|
+
name: 'securenow_alert_rule_draft',
|
|
1383
|
+
title: 'Draft an Alert Rule From English',
|
|
1384
|
+
description: 'Turn a detection idea in plain English into an alert rule draft for one app, checked against that app\'s data. It reads the app\'s real event types, attribute keys and routes, then picks the kind: instant (conditions checked on every incoming request, alerts within seconds; one request is enough evidence) or periodic (SQL run on a schedule: counts, thresholds, distinct counts, ratios, sequences, baselines, security events, app log lines). Periodic SQL is run once against the data; instant conditions are replayed over the app\'s last 2000 requests, so check.matches shows what would alert today (false positives). Nothing is saved: create it with securenow_alert_rule_create, passing the draft fields. Waits up to 120 seconds.',
|
|
1385
|
+
scope: 'alerts:write',
|
|
1386
|
+
readOnly: false,
|
|
1387
|
+
confirm: true,
|
|
1388
|
+
method: 'POST',
|
|
1389
|
+
endpoint: '/alert-rules/draft',
|
|
1390
|
+
bodyFields: ['text', 'applicationKey', 'environment', 'kind'],
|
|
1391
|
+
poll: {
|
|
1392
|
+
tool: 'securenow_alert_rule_draft_status',
|
|
1393
|
+
idField: 'draftId',
|
|
1394
|
+
pending: ['running'],
|
|
1395
|
+
timeoutMs: 120000,
|
|
1396
|
+
intervalMs: 2000,
|
|
1397
|
+
},
|
|
1398
|
+
inputSchema: objectSchema({
|
|
1399
|
+
text: string('What the rule should catch, in plain English, e.g. "one IP failing 10 sign-ins in 5 minutes" or "anyone requesting /.env or /.git".'),
|
|
1400
|
+
applicationKey: string('App key the rule watches; the draft is checked against its data.'),
|
|
1401
|
+
environment: string('Only watch this environment (e.g. production). Omit to watch every environment.'),
|
|
1402
|
+
kind: string('Optional: instant or periodic. Omit to let the drafter choose.'),
|
|
1403
|
+
...confirmSchema,
|
|
1404
|
+
}, ['text', 'applicationKey', 'confirm', 'reason']),
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
name: 'securenow_alert_rule_draft_status',
|
|
1408
|
+
title: 'Alert Rule Draft Result',
|
|
1409
|
+
description: 'Get an alert rule draft started by securenow_alert_rule_draft: status (running, complete or failed), then the draft (kind, name, summary, SQL and schedule, or instant conditions) and the check against the app\'s data.',
|
|
1410
|
+
scope: 'alerts:read',
|
|
1411
|
+
readOnly: true,
|
|
1412
|
+
method: 'GET',
|
|
1413
|
+
endpoint: '/alert-rules/draft/:draftId',
|
|
1414
|
+
pathParams: ['draftId'],
|
|
1415
|
+
inputSchema: objectSchema({
|
|
1416
|
+
draftId: string('Draft id from securenow_alert_rule_draft.'),
|
|
1417
|
+
}, ['draftId']),
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
name: 'securenow_alert_rule_create',
|
|
1421
|
+
title: 'Create Alert Rule (Test Mode)',
|
|
1422
|
+
description: 'Create an alert rule. It always starts in test mode: it alerts, but nothing blocks, rate-limits or acts on its alerts until a person promotes it. Instant rule: executionMode "instant" plus instant {matchMode: any|all, conditions: [{field: path|method|user_agent|status_code|request_body|request_headers|client_ip|host, operator, value}]}. Periodic rule: sqlQuery plus schedule {cronExpression}; the SQL must scope every table with the __USER_APP_KEYS__ placeholder, bound time on every table read, and return a column named ip (rows without an ip never alert). Easiest path: securenow_alert_rule_draft, then pass its draft fields here. Write action; requires confirmation.',
|
|
1423
|
+
scope: 'alerts:write',
|
|
1424
|
+
readOnly: false,
|
|
1425
|
+
confirm: true,
|
|
1426
|
+
method: 'POST',
|
|
1427
|
+
endpoint: '/alert-rules',
|
|
1428
|
+
bodyFields: ['name', 'description', 'nlpQuery', 'category', 'severity', 'applications', 'applicationsAll', 'executionMode', 'sqlQuery', 'schedule', 'instant', 'throttle'],
|
|
1429
|
+
inputSchema: objectSchema({
|
|
1430
|
+
name: string('Rule name.'),
|
|
1431
|
+
description: string('What it catches, in a sentence.'),
|
|
1432
|
+
nlpQuery: string('The detection idea in plain English (kept with the rule).'),
|
|
1433
|
+
category: string('One word, e.g. authentication, scanning, exfiltration.'),
|
|
1434
|
+
severity: string('low, medium, high or critical.'),
|
|
1435
|
+
applications: { type: 'array', items: { type: 'string' }, description: 'App keys the rule watches.' },
|
|
1436
|
+
applicationsAll: boolean('true watches every app instead of applications.'),
|
|
1437
|
+
executionMode: string('scheduled (periodic SQL, the default) or instant (conditions on every request).'),
|
|
1438
|
+
sqlQuery: string('Periodic rules: the detection SELECT.'),
|
|
1439
|
+
schedule: { type: 'object', additionalProperties: true, description: 'Periodic rules: {cronExpression: "* * * * *", description: "Every minute"}. Default every 15 minutes.' },
|
|
1440
|
+
instant: { type: 'object', additionalProperties: true, description: 'Instant rules: {matchMode: "any" | "all", conditions: [{field, operator, value}], environments: ["staging"] (optional; omit to watch every environment)}.' },
|
|
1441
|
+
throttle: { type: 'object', additionalProperties: true, description: 'Optional {enabled: true, minutes: 15}: one alert per rule per window.' },
|
|
1442
|
+
...confirmSchema,
|
|
1443
|
+
}, ['name', 'confirm', 'reason']),
|
|
1444
|
+
},
|
|
1381
1445
|
{
|
|
1382
1446
|
name: 'securenow_alert_rules_list',
|
|
1383
1447
|
title: 'List Alert Rules',
|
package/package.json
CHANGED