Files
Max Mayfield ccc4cd1c32 Scaffold dd0c/alert: ingestion, correlation engine, HMAC validation, tests
- 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
2026-03-01 02:49:14 +00:00

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');
});
});