feat: stack glob patterns and route severity levels (#1651)

* feat: add stack glob patterns and route severity levels

Operators can filter notification routes and mute rules with anchored * globs, and routes can target info, warning, or error. Matching is fail-closed for unsafe stored patterns; write paths keep partial-PUT semantics and ReDoS caps.

* test: split mute and routing chip tests to avoid dialog race

* fix: move stack pattern client validator out of PatternChips

* fix: bound stack glob matching and atomic pattern chip saves
This commit is contained in:
Anso
2026-07-19 14:54:48 -04:00
committed by GitHub
parent 63213c0960
commit 972f2b9483
26 changed files with 1158 additions and 118 deletions
@@ -173,4 +173,126 @@ describe('Notification suppression - CRUD', () => {
.set('Cookie', authCookie);
expect(res.status).toBe(200);
});
it('POST omits stack_patterns to []; rejects malformed and ReDoS patterns', async () => {
const omitted = await request(app)
.post('/api/notification-suppression-rules')
.set('Cookie', authCookie)
.send({
name: 'Mute omit',
applies_to: 'both',
});
expect(omitted.status).toBe(201);
expect(omitted.body.stack_patterns).toEqual([]);
DatabaseService.getInstance().deleteNotificationSuppressionRule(omitted.body.id);
const bad = await request(app)
.post('/api/notification-suppression-rules')
.set('Cookie', authCookie)
.send({ ...validBody, name: 'bad', stack_patterns: null });
expect(bad.status).toBe(400);
const redos = await request(app)
.post('/api/notification-suppression-rules')
.set('Cookie', authCookie)
.send({ ...validBody, name: 'redos', stack_patterns: ['****'] });
expect(redos.status).toBe(400);
});
it('PUT enabled-only preserves patterns; explicit [] clears', async () => {
const created = await request(app)
.post('/api/notification-suppression-rules')
.set('Cookie', authCookie)
.send(validBody);
const id = created.body.id as number;
const partial = await request(app)
.put(`/api/notification-suppression-rules/${id}`)
.set('Cookie', authCookie)
.send({ enabled: false });
expect(partial.status).toBe(200);
expect(partial.body.enabled).toBe(false);
expect(partial.body.stack_patterns).toEqual(['staging']);
expect(partial.body.levels).toEqual(['warning']);
const cleared = await request(app)
.put(`/api/notification-suppression-rules/${id}`)
.set('Cookie', authCookie)
.send({ stack_patterns: [] });
expect(cleared.status).toBe(200);
expect(cleared.body.stack_patterns).toEqual([]);
DatabaseService.getInstance().deleteNotificationSuppressionRule(id);
});
it('replica requires and validates stack_patterns', async () => {
const jwt = await import('jsonwebtoken');
const { TEST_JWT_SECRET } = await import('./helpers/testConstants');
const token = jwt.default.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '1m' });
const missing = await request(app)
.post('/api/notification-suppression-rules/replica')
.set('Authorization', `Bearer ${token}`)
.send({
rule: {
id: 9001,
name: 'replica',
applies_to: 'both',
node_id: null,
label_ids: null,
categories: null,
levels: null,
enabled: true,
expires_at: null,
created_at: 1,
updated_at: 1,
},
});
expect(missing.status).toBe(400);
const redos = await request(app)
.post('/api/notification-suppression-rules/replica')
.set('Authorization', `Bearer ${token}`)
.send({
rule: {
id: 9001,
name: 'replica',
applies_to: 'both',
stack_patterns: ['****'],
node_id: null,
label_ids: null,
categories: null,
levels: null,
enabled: true,
expires_at: null,
created_at: 1,
updated_at: 1,
},
});
expect(redos.status).toBe(400);
expect(DatabaseService.getInstance().getNotificationSuppressionRule(9001)).toBeUndefined();
const ok = await request(app)
.post('/api/notification-suppression-rules/replica')
.set('Authorization', `Bearer ${token}`)
.send({
rule: {
id: 9001,
name: 'replica',
applies_to: 'both',
stack_patterns: ['prod-*'],
node_id: null,
label_ids: null,
categories: null,
levels: null,
enabled: true,
expires_at: null,
created_at: 1,
updated_at: 1,
},
});
expect(ok.status).toBe(200);
expect(DatabaseService.getInstance().getNotificationSuppressionRule(9001)?.stack_patterns).toEqual(['prod-*']);
DatabaseService.getInstance().deleteNotificationSuppressionRule(9001);
});
});