From 3899691fb5bc72f71ad93a6ced138095f7287f05 Mon Sep 17 00:00:00 2001 From: Endowed992 Date: Wed, 17 Jun 2026 08:19:30 +0000 Subject: [PATCH] feat: implemented ai alert summaries generation --- apps/backend/src/app.module.ts | 3 +- apps/backend/src/modules/ai/ai.module.ts | 8 ++ .../summaries/alert-summary.service.spec.ts | 31 +++++ .../ai/summaries/alert-summary.service.ts | 111 ++++++++++++++++++ .../backend/src/modules/ai/summaries/index.ts | 1 + .../notification-provider.interface.ts | 3 + .../discord.notification-provider.ts | 39 ++++-- .../telegram.notification-provider.ts | 15 ++- 8 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 apps/backend/src/modules/ai/ai.module.ts create mode 100644 apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts create mode 100644 apps/backend/src/modules/ai/summaries/alert-summary.service.ts create mode 100644 apps/backend/src/modules/ai/summaries/index.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 161d550..e5691cf 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,12 +1,13 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { DatabaseModule } from '../../../database/database.module'; +import { AiModule } from './modules/ai/ai.module'; import { HealthModule } from './modules/health/health.module'; import { NotificationsModule } from './modules/notifications/notifications.module'; import { ReportingModule } from './modules/reporting/reporting.module'; @Module({ - imports: [DatabaseModule, HealthModule, NotificationsModule, ReportingModule], + imports: [DatabaseModule, AiModule, HealthModule, NotificationsModule, ReportingModule], controllers: [AppController], }) export class AppModule {} diff --git a/apps/backend/src/modules/ai/ai.module.ts b/apps/backend/src/modules/ai/ai.module.ts new file mode 100644 index 0000000..3c9a366 --- /dev/null +++ b/apps/backend/src/modules/ai/ai.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { AlertSummaryService } from './summaries'; + +@Module({ + providers: [AlertSummaryService], + exports: [AlertSummaryService], +}) +export class AiModule {} diff --git a/apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts b/apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts new file mode 100644 index 0000000..4aafd1d --- /dev/null +++ b/apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts @@ -0,0 +1,31 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AlertSummaryService } from './alert-summary.service'; + +describe('AlertSummaryService', () => { + let service: AlertSummaryService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AlertSummaryService], + }).compile(); + + service = module.get(AlertSummaryService); + }); + + it('creates a human-readable summary, risk explanation, and recommendations', () => { + const result = service.generateAlertSummary({ + title: 'Critical Alert: Ownership Transfer Detected', + message: 'Contract 0x1234 is attempting to transfer ownership to 0x5678 before confirmation.', + severity: 'critical', + metadata: { + signature: 'renounceOwnership', + contract: '0x1234', + }, + }); + + expect(result.summary).toContain('ownership transfer'); + expect(result.riskExplanation).toContain('loss of control'); + expect(result.recommendations.length).toBeGreaterThan(0); + expect(result.recommendations.some(action => /pause|review|verify/i.test(action))).toBe(true); + }); +}); diff --git a/apps/backend/src/modules/ai/summaries/alert-summary.service.ts b/apps/backend/src/modules/ai/summaries/alert-summary.service.ts new file mode 100644 index 0000000..44ea08f --- /dev/null +++ b/apps/backend/src/modules/ai/summaries/alert-summary.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; + +export interface AlertSummaryInput { + title: string; + message: string; + severity: 'low' | 'medium' | 'high' | 'critical'; + metadata?: Record; +} + +export interface AlertSummaryResult { + summary: string; + riskExplanation: string; + recommendations: string[]; +} + +@Injectable() +export class AlertSummaryService { + generateAlertSummary(input: AlertSummaryInput): AlertSummaryResult { + const signature = this.getMetadataValue(input.metadata, 'signature'); + const contract = this.getMetadataValue(input.metadata, 'contract'); + const summary = this.buildSummary(input.title, input.message, signature, contract); + const riskExplanation = this.buildRiskExplanation(input.severity, signature, contract); + const recommendations = this.buildRecommendations(input.severity, signature); + + return { + summary, + riskExplanation, + recommendations, + }; + } + + private buildSummary( + title: string, + message: string, + signature?: string, + contract?: string, + ): string { + const signatureText = signature ? ` Pattern detected: ${signature}.` : ''; + const contractText = contract ? ` Contract ${contract} is involved.` : ''; + const ownershipTransferText = + /ownership|transfer/i.test(message) || /ownership|transfer/i.test(title) + ? ' This indicates an ownership transfer risk.' + : ''; + return `${title}. ${message}${ownershipTransferText}${signatureText}${contractText}` + .replace(/\s+/g, ' ') + .trim(); + } + + private buildRiskExplanation( + severity: AlertSummaryInput['severity'], + signature?: string, + contract?: string, + ): string { + const severityText = this.describeSeverity(severity); + const signatureText = signature + ? ` The ${signature} pattern suggests a potentially malicious transaction.` + : ''; + const contractText = contract + ? ` The activity involving ${contract} could lead to loss of control or fund movement if not contained.` + : ''; + return `${severityText}${signatureText}${contractText}`.replace(/\s+/g, ' ').trim(); + } + + private buildRecommendations( + severity: AlertSummaryInput['severity'], + signature?: string, + ): string[] { + const base = [ + 'Review the transaction details immediately and confirm whether the activity is expected.', + 'Verify the contract state and any recent admin or ownership changes before taking further action.', + ]; + + if (severity === 'high' || severity === 'critical') { + base.push('Pause or restrict contract interactions if the risk is confirmed.'); + base.push('Escalate to the on-call operator and prepare an incident response checklist.'); + } + + if (signature) { + base.push( + `Investigate the ${signature} behavior specifically and compare it against known safe patterns.`, + ); + } + + return base; + } + + private describeSeverity(severity: AlertSummaryInput['severity']): string { + switch (severity) { + case 'critical': + return 'This is a critical alert because the activity may immediately expose assets to compromise.'; + case 'high': + return 'This is a high-risk alert because the activity could affect contract safety or user funds.'; + case 'medium': + return 'This is a medium-risk alert because the activity warrants closer review.'; + default: + return 'This is a low-risk alert because the activity appears limited and should still be checked.'; + } + } + + private getMetadataValue( + metadata: Record | undefined, + key: string, + ): string | undefined { + if (!metadata) { + return undefined; + } + + const value = metadata[key]; + return typeof value === 'string' ? value : undefined; + } +} diff --git a/apps/backend/src/modules/ai/summaries/index.ts b/apps/backend/src/modules/ai/summaries/index.ts new file mode 100644 index 0000000..a7b4da3 --- /dev/null +++ b/apps/backend/src/modules/ai/summaries/index.ts @@ -0,0 +1 @@ +export * from './alert-summary.service'; diff --git a/apps/backend/src/modules/notifications/interfaces/notification-provider.interface.ts b/apps/backend/src/modules/notifications/interfaces/notification-provider.interface.ts index ecc8def..6c70f75 100644 --- a/apps/backend/src/modules/notifications/interfaces/notification-provider.interface.ts +++ b/apps/backend/src/modules/notifications/interfaces/notification-provider.interface.ts @@ -5,6 +5,9 @@ export interface NotificationPayload { title: string; message: string; severity: 'low' | 'medium' | 'high' | 'critical'; + summary?: string; + riskExplanation?: string; + recommendations?: string[]; metadata?: Record; } diff --git a/apps/backend/src/modules/notifications/providers/discord.notification-provider.ts b/apps/backend/src/modules/notifications/providers/discord.notification-provider.ts index 2f216ac..4108955 100644 --- a/apps/backend/src/modules/notifications/providers/discord.notification-provider.ts +++ b/apps/backend/src/modules/notifications/providers/discord.notification-provider.ts @@ -34,20 +34,43 @@ export class DiscordNotificationProvider implements INotificationProvider { } async sendAlert(payload: NotificationPayload): Promise { + const description = payload.summary ?? payload.message; + const fields = [ + ...(payload.riskExplanation + ? [ + { + name: 'Risk explanation', + value: payload.riskExplanation, + inline: false, + }, + ] + : []), + ...(payload.recommendations && payload.recommendations.length > 0 + ? [ + { + name: 'Recommended actions', + value: payload.recommendations.join('\n• '), + inline: false, + }, + ] + : []), + ...(payload.metadata + ? Object.entries(payload.metadata).map(([name, value]) => ({ + name, + value: String(value), + inline: true, + })) + : []), + ]; + const body = { embeds: [ { title: payload.title, - description: payload.message, + description, color: SEVERITY_COLORS[payload.severity], timestamp: new Date().toISOString(), - fields: payload.metadata - ? Object.entries(payload.metadata).map(([name, value]) => ({ - name, - value: String(value), - inline: true, - })) - : [], + fields, }, ], }; diff --git a/apps/backend/src/modules/notifications/providers/telegram.notification-provider.ts b/apps/backend/src/modules/notifications/providers/telegram.notification-provider.ts index ad30c7e..bbccd0f 100644 --- a/apps/backend/src/modules/notifications/providers/telegram.notification-provider.ts +++ b/apps/backend/src/modules/notifications/providers/telegram.notification-provider.ts @@ -36,9 +36,22 @@ export class TelegramNotificationProvider implements INotificationProvider { const lines: string[] = [ `${emoji} *${this.escapeMarkdown(payload.title)}*`, '', - this.escapeMarkdown(payload.message), + this.escapeMarkdown(payload.summary ?? payload.message), ]; + if (payload.riskExplanation) { + lines.push(''); + lines.push(`*Risk explanation:* ${this.escapeMarkdown(payload.riskExplanation)}`); + } + + if (payload.recommendations?.length) { + lines.push(''); + lines.push('*Recommended actions:*'); + for (const recommendation of payload.recommendations) { + lines.push(`• ${this.escapeMarkdown(recommendation)}`); + } + } + if (payload.metadata) { lines.push(''); for (const [key, value] of Object.entries(payload.metadata)) {