Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
8 changes: 8 additions & 0 deletions apps/backend/src/modules/ai/ai.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { AlertSummaryService } from './summaries';

@Module({
providers: [AlertSummaryService],
exports: [AlertSummaryService],
})
export class AiModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AlertSummaryService } from './alert-summary.service';

describe('AlertSummaryService', () => {

Check failure on line 4 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.
let service: AlertSummaryService;

beforeEach(async () => {

Check failure on line 7 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'beforeEach'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.
const module: TestingModule = await Test.createTestingModule({
providers: [AlertSummaryService],
}).compile();

service = module.get<AlertSummaryService>(AlertSummaryService);
});

it('creates a human-readable summary, risk explanation, and recommendations', () => {

Check failure on line 15 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'it'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.
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');

Check failure on line 26 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
expect(result.riskExplanation).toContain('loss of control');

Check failure on line 27 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
expect(result.recommendations.length).toBeGreaterThan(0);

Check failure on line 28 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
expect(result.recommendations.some(action => /pause|review|verify/i.test(action))).toBe(true);

Check failure on line 29 in apps/backend/src/modules/ai/summaries/alert-summary.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
});
});
111 changes: 111 additions & 0 deletions apps/backend/src/modules/ai/summaries/alert-summary.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Injectable } from '@nestjs/common';

export interface AlertSummaryInput {
title: string;
message: string;
severity: 'low' | 'medium' | 'high' | 'critical';
metadata?: Record<string, unknown>;
}

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<string, unknown> | undefined,
key: string,
): string | undefined {
if (!metadata) {
return undefined;
}

const value = metadata[key];
return typeof value === 'string' ? value : undefined;
}
}
1 change: 1 addition & 0 deletions apps/backend/src/modules/ai/summaries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './alert-summary.service';
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export interface NotificationPayload {
title: string;
message: string;
severity: 'low' | 'medium' | 'high' | 'critical';
summary?: string;
riskExplanation?: string;
recommendations?: string[];
metadata?: Record<string, unknown>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,43 @@ export class DiscordNotificationProvider implements INotificationProvider {
}

async sendAlert(payload: NotificationPayload): Promise<void> {
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,
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading