import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify' import { getDatabase } from '../lib/db' import { HijackAlertsDatabaseClient } from '../features/hijack-alerts/db-client' import { WebhookClient } from '../features/hijack-alerts/webhook-client' import { checkForHijacks } from '../features/hijack-alerts/detector' import crypto from 'crypto' const db = new HijackAlertsDatabaseClient(getDatabase()) const webhookClient = new WebhookClient() function generateSecretKey(): string { return 'sk_' + crypto.randomBytes(32).toString('hex') } interface RegisterWebhookRequest { endpoint_url: string timeout_ms?: number max_retries?: number } interface CreateHijackRequest { asn: number prefix: string } export async function hijackAlertsRoutes(fastify: FastifyInstance): Promise { // Register webhook subscription fastify.post<{ Querystring: { asn: string } }>( '/webhooks', async (request: FastifyRequest<{ Querystring: { asn: string }; Body: RegisterWebhookRequest }>, reply: FastifyReply) => { try { const asn = parseInt(request.query.asn, 10) const body = request.body as RegisterWebhookRequest if (isNaN(asn)) { return reply.status(400).send({ error: 'Invalid ASN' }) } if (!body.endpoint_url) { return reply.status(400).send({ error: 'endpoint_url is required' }) } const secretKey = generateSecretKey() const webhook = await db.createWebhookSubscription( { asn, endpoint_url: body.endpoint_url, timeout_ms: body.timeout_ms, max_retries: body.max_retries, }, secretKey ) return reply.status(201).send({ id: webhook.id, asn: webhook.asn, endpoint_url: webhook.endpoint_url, secret_key: secretKey, created_at: webhook.created_at, active: webhook.active, }) } catch (error) { console.error('[Routes] Error registering webhook:', error) return reply.status(500).send({ error: 'Failed to register webhook' }) } } ) // List webhooks for ASN fastify.get<{ Querystring: { asn: string } }>( '/webhooks', async (request: FastifyRequest<{ Querystring: { asn: string } }>, reply: FastifyReply) => { try { const asn = parseInt(request.query.asn, 10) if (isNaN(asn)) { return reply.status(400).send({ error: 'Invalid ASN' }) } const webhooks = await db.getWebhooksByAsn(asn) return reply.send({ asn, webhooks: webhooks.map((w) => ({ id: w.id, endpoint_url: w.endpoint_url, active: w.active, failure_count: w.failure_count, last_triggered_at: w.last_triggered_at, max_retries: w.max_retries, timeout_ms: w.timeout_ms, })), }) } catch (error) { console.error('[Routes] Error listing webhooks:', error) return reply.status(500).send({ error: 'Failed to list webhooks' }) } } ) // Delete webhook subscription fastify.delete<{ Params: { id: string } }>( '/webhooks/:id', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { try { const webhookId = parseInt(request.params.id, 10) if (isNaN(webhookId)) { return reply.status(400).send({ error: 'Invalid webhook ID' }) } await db.deleteWebhookSubscription(webhookId) return reply.send({ deleted: true, id: webhookId }) } catch (error) { console.error('[Routes] Error deleting webhook:', error) return reply.status(500).send({ error: 'Failed to delete webhook' }) } } ) // Test webhook delivery fastify.post<{ Params: { id: string } }>( '/webhooks/:id/test', async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => { try { const webhookId = parseInt(request.params.id, 10) if (isNaN(webhookId)) { return reply.status(400).send({ error: 'Invalid webhook ID' }) } const webhook = await db.getWebhookSubscription(webhookId) if (!webhook) { return reply.status(404).send({ error: 'Webhook not found' }) } const testEvent = { id: 0, asn: webhook.asn, prefix: '0.0.0.0/0', detected_at: new Date(), expected_asn: webhook.asn, detected_asns: [webhook.asn], hijack_type: 'HIJACK' as const, severity: 'HIGH' as const, description: 'Test event from PeerCortex', details: { source: 'api-test', timestamp: new Date().toISOString() }, resolved: false, resolved_at: null, created_at: new Date(), } const result = await webhookClient.sendWebhook( testEvent, webhook.endpoint_url, webhook.secret_key, webhook.timeout_ms ) return reply.send({ success: result.success, status: result.status, error: result.error ?? null, response_time_ms: result.response_time_ms, }) } catch (error) { console.error('[Routes] Error testing webhook:', error) return reply.status(500).send({ error: 'Failed to test webhook' }) } } ) // List hijack events for ASN fastify.get<{ Querystring: { asn: string; limit?: string; offset?: string; resolved?: string } }>( '/hijacks', async (request: FastifyRequest<{ Querystring: { asn: string; limit?: string; offset?: string; resolved?: string } }>, reply: FastifyReply) => { try { const asn = parseInt(request.query.asn, 10) const limit = parseInt(request.query.limit ?? '50', 10) const offset = parseInt(request.query.offset ?? '0', 10) const resolved = request.query.resolved ? request.query.resolved === 'true' : undefined if (isNaN(asn)) { return reply.status(400).send({ error: 'Invalid ASN' }) } const result = await db.getHijacksByAsn(asn, Math.min(limit, 500), offset, resolved) return reply.send({ asn, total: result.total, limit, offset, events: result.events.map((e) => ({ id: e.id, prefix: e.prefix, hijack_type: e.hijack_type, severity: e.severity, detected_at: e.detected_at, resolved: e.resolved, resolved_at: e.resolved_at, description: e.description, })), }) } catch (error) { console.error('[Routes] Error listing hijacks:', error) return reply.status(500).send({ error: 'Failed to list hijacks' }) } } ) // Manually trigger hijack detection fastify.post<{ Body: CreateHijackRequest }>( '/hijacks/detect', async (request: FastifyRequest<{ Body: CreateHijackRequest }>, reply: FastifyReply) => { try { const body = request.body as CreateHijackRequest if (!body.asn || !body.prefix) { return reply.status(400).send({ error: 'asn and prefix are required' }) } const results = await checkForHijacks(`${body.asn}:${body.prefix}`, db) if (results[0]?.detected && results[0]?.event) { const hijack = await db.insertHijackEvent(results[0].event) return reply.status(201).send({ detected: true, event: hijack }) } return reply.send({ detected: false, reason: results[0]?.reason ?? 'No hijack detected' }) } catch (error) { console.error('[Routes] Error detecting hijacks:', error) return reply.status(500).send({ error: 'Failed to detect hijacks' }) } } ) // Resolve hijack fastify.post<{ Params: { id: string }; Body: { resolution_notes: string } }>( '/hijacks/:id/resolve', async (request: FastifyRequest<{ Params: { id: string }; Body: { resolution_notes: string } }>, reply: FastifyReply) => { try { const eventId = parseInt(request.params.id, 10) if (isNaN(eventId)) { return reply.status(400).send({ error: 'Invalid event ID' }) } const hijack = await db.resolveHijack(eventId, request.body.resolution_notes) return reply.send({ id: hijack.id, resolved: hijack.resolved, resolved_at: hijack.resolved_at, }) } catch (error) { console.error('[Routes] Error resolving hijack:', error) return reply.status(500).send({ error: 'Failed to resolve hijack' }) } } ) }