- Webhook ingestion: HMAC validation for Datadog/PagerDuty/OpsGenie with 5-min timestamp freshness - Payload normalizers: canonical alert schema with severity mapping per provider - Correlation engine: time-window grouping, late-alert attachment (2x window), FakeClock for testing - InMemoryWindowStore for unit tests - Tests: 12 HMAC validation cases, 5 normalizer cases, 7 correlation engine cases - PostgreSQL schema with RLS: tenants, incidents, alerts, webhook_secrets, notification_configs - Free tier enforcement columns (alert_count_month, reset_at) - Fly.io config, Dockerfile, Gitea Actions CI
139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
validateDatadogHmac,
|
|
validatePagerdutyHmac,
|
|
validateOpsgenieHmac,
|
|
normalizeDatadog,
|
|
normalizePagerduty,
|
|
normalizeOpsgenie,
|
|
} from '../../src/ingestion/webhook.js';
|
|
import crypto from 'node:crypto';
|
|
|
|
const SECRET = 'test-webhook-secret';
|
|
|
|
describe('HMAC Validation', () => {
|
|
describe('Datadog', () => {
|
|
it('accepts valid signature with fresh timestamp', () => {
|
|
const body = '{"alert_id":"123"}';
|
|
const ts = Math.floor(Date.now() / 1000).toString();
|
|
const sig = crypto.createHmac('sha256', SECRET).update(ts + body).digest('hex');
|
|
|
|
const result = validateDatadogHmac(body, sig, ts, SECRET);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('rejects stale timestamp (>5 minutes)', () => {
|
|
const body = '{"alert_id":"123"}';
|
|
const ts = (Math.floor(Date.now() / 1000) - 301).toString();
|
|
const sig = crypto.createHmac('sha256', SECRET).update(ts + body).digest('hex');
|
|
|
|
const result = validateDatadogHmac(body, sig, ts, SECRET);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.error).toContain('stale timestamp');
|
|
});
|
|
|
|
it('rejects missing signature', () => {
|
|
const result = validateDatadogHmac('{}', undefined, '123', SECRET);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid signature', () => {
|
|
const ts = Math.floor(Date.now() / 1000).toString();
|
|
const result = validateDatadogHmac('{}', 'bad-sig', ts, SECRET);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('PagerDuty', () => {
|
|
it('rejects missing signature', () => {
|
|
const result = validatePagerdutyHmac('{}', undefined, SECRET);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('rejects malformed signature', () => {
|
|
const result = validatePagerdutyHmac('{}', 'garbage', SECRET);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('OpsGenie', () => {
|
|
it('rejects stale timestamp from payload body', () => {
|
|
const staleTs = Date.now() - 6 * 60 * 1000; // 6 minutes ago
|
|
const body = JSON.stringify({ alert: { alertId: '1' }, timestamp: staleTs });
|
|
const sig = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
|
|
|
|
const result = validateOpsgenieHmac(body, sig, SECRET);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.error).toContain('stale timestamp');
|
|
});
|
|
|
|
it('accepts fresh payload', () => {
|
|
const body = JSON.stringify({ alert: { alertId: '1' }, timestamp: Date.now() });
|
|
const sig = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
|
|
|
|
const result = validateOpsgenieHmac(body, sig, SECRET);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Payload Normalizers', () => {
|
|
it('normalizes Datadog payload', () => {
|
|
const alert = normalizeDatadog({
|
|
alert_id: 'dd-123',
|
|
title: 'High CPU',
|
|
priority: 'P1',
|
|
alert_transition: 'Triggered',
|
|
tags: { service: 'auth', env: 'prod' },
|
|
date_happened: 1709251200,
|
|
});
|
|
expect(alert.sourceProvider).toBe('datadog');
|
|
expect(alert.severity).toBe('critical');
|
|
expect(alert.status).toBe('firing');
|
|
expect(alert.service).toBe('auth');
|
|
});
|
|
|
|
it('normalizes PagerDuty payload', () => {
|
|
const alert = normalizePagerduty({
|
|
event: {
|
|
event_type: 'incident.triggered',
|
|
data: {
|
|
id: 'pd-456',
|
|
title: 'Disk Full',
|
|
urgency: 'high',
|
|
service: { name: 'storage' },
|
|
created_at: '2026-03-01T00:00:00Z',
|
|
},
|
|
},
|
|
});
|
|
expect(alert.sourceProvider).toBe('pagerduty');
|
|
expect(alert.severity).toBe('critical');
|
|
expect(alert.service).toBe('storage');
|
|
});
|
|
|
|
it('normalizes OpsGenie payload', () => {
|
|
const alert = normalizeOpsgenie({
|
|
action: 'Create',
|
|
alert: {
|
|
alertId: 'og-789',
|
|
message: 'Memory Leak',
|
|
priority: 'P2',
|
|
tags: ['service:api'],
|
|
},
|
|
});
|
|
expect(alert.sourceProvider).toBe('opsgenie');
|
|
expect(alert.severity).toBe('high');
|
|
expect(alert.status).toBe('firing');
|
|
});
|
|
|
|
it('maps Datadog Recovered to resolved', () => {
|
|
const alert = normalizeDatadog({ alert_transition: 'Recovered' });
|
|
expect(alert.status).toBe('resolved');
|
|
});
|
|
|
|
it('maps OpsGenie Close to resolved', () => {
|
|
const alert = normalizeOpsgenie({ action: 'Close', alert: {} });
|
|
expect(alert.status).toBe('resolved');
|
|
});
|
|
});
|