Skip to content
Merged
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
16 changes: 14 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
project: ['tsconfig.json', 'apps/dashboard/tsconfig.json'],
tsconfigRootDir: __dirname,
sourceType: 'module',
},
Expand All @@ -20,4 +20,16 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
},
};
overrides: [
{
// Files outside all tsconfigs — lint without type-aware rules
files: ['observability/**/*.ts', 'prisma/**/*.ts', 'src/**/*.ts', 'env.d.ts'],
parserOptions: {
project: null,
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
],
};
2 changes: 1 addition & 1 deletion apps/backend/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export class AppController {
healthCheck(): { status: string; timestamp: string } {
return { status: 'ok', timestamp: new Date().toISOString() };
}
}
}
3 changes: 2 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { AppController } from './app.controller';
import { DatabaseModule } from '../../../database/database.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],
imports: [DatabaseModule, HealthModule, NotificationsModule, ReportingModule],
controllers: [AppController],
})
export class AppModule {}
2 changes: 1 addition & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ async function bootstrap() {
const port = process.env.PORT ?? 3000;
await app.listen(port);
}
bootstrap();
bootstrap();
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type AlertSeverity = 'low' | 'medium' | 'high' | 'critical';

export interface SeverityBreakdown {
low: number;
medium: number;
high: number;
critical: number;
}

export interface ChainBreakdown {
chain: string;
count: number;
}

export interface SecurityReport {
generatedAt: string;
periodDays: number;
totalAlerts: number;
severityBreakdown: SeverityBreakdown;
topChains: ChainBreakdown[];
resolvedAlerts: number;
unresolvedAlerts: number;
criticalUnresolved: number;
}
21 changes: 21 additions & 0 deletions apps/backend/src/modules/reporting/reporting.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Controller, Get, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
import { ReportingService } from './reporting.service';
import { SecurityReport } from './interfaces/reporting.interface';

/**
* Exposes executive security report endpoints.
*
* GET /reporting/security — report for the last 30 days (default)
* GET /reporting/security?days=7 — report for a custom window
*/
@Controller('reporting')
export class ReportingController {
constructor(private readonly reportingService: ReportingService) {}

@Get('security')
getSecurityReport(
@Query('days', new DefaultValuePipe(30), ParseIntPipe) days: number,
): SecurityReport {
return this.reportingService.getSecurityReport(days);
}
}
10 changes: 10 additions & 0 deletions apps/backend/src/modules/reporting/reporting.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ReportingService } from './reporting.service';
import { ReportingController } from './reporting.controller';

@Module({
controllers: [ReportingController],
providers: [ReportingService],
exports: [ReportingService],
})
export class ReportingModule {}
67 changes: 67 additions & 0 deletions apps/backend/src/modules/reporting/reporting.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'reflect-metadata';
import { Test, TestingModule } from '@nestjs/testing';
import { ReportingService } from './reporting.service';

describe('ReportingService', () => {

Check failure on line 5 in apps/backend/src/modules/reporting/reporting.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: ReportingService;

beforeEach(async () => {

Check failure on line 8 in apps/backend/src/modules/reporting/reporting.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: [ReportingService],
}).compile();

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

it('should be defined', () => {

Check failure on line 16 in apps/backend/src/modules/reporting/reporting.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.
expect(service).toBeDefined();

Check failure on line 17 in apps/backend/src/modules/reporting/reporting.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
});

describe('getSecurityReport', () => {

Check failure on line 20 in apps/backend/src/modules/reporting/reporting.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.
it('returns a report with the requested periodDays', () => {

Check failure on line 21 in apps/backend/src/modules/reporting/reporting.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 report = service.getSecurityReport(7);
expect(report.periodDays).toBe(7);

Check failure on line 23 in apps/backend/src/modules/reporting/reporting.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
});

it('defaults to 30 days when called with no argument', () => {

Check failure on line 26 in apps/backend/src/modules/reporting/reporting.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 report = service.getSecurityReport();
expect(report.periodDays).toBe(30);

Check failure on line 28 in apps/backend/src/modules/reporting/reporting.service.spec.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Cannot find name 'expect'.
});

it('totalAlerts equals sum of severityBreakdown', () => {

Check failure on line 31 in apps/backend/src/modules/reporting/reporting.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 report = service.getSecurityReport();
const { low, medium, high, critical } = report.severityBreakdown;
expect(report.totalAlerts).toBe(low + medium + high + critical);
});

it('resolvedAlerts + unresolvedAlerts equals totalAlerts', () => {
const report = service.getSecurityReport();
expect(report.resolvedAlerts + report.unresolvedAlerts).toBe(report.totalAlerts);
});

it('returns a valid ISO timestamp in generatedAt', () => {
const report = service.getSecurityReport();
expect(() => new Date(report.generatedAt)).not.toThrow();
expect(new Date(report.generatedAt).toISOString()).toBe(report.generatedAt);
});

it('topChains is a non-empty array', () => {
const report = service.getSecurityReport();
expect(Array.isArray(report.topChains)).toBe(true);
expect(report.topChains.length).toBeGreaterThan(0);
});

it('each topChain has a chain name and a numeric count', () => {
const report = service.getSecurityReport();
report.topChains.forEach(c => {
expect(typeof c.chain).toBe('string');
expect(typeof c.count).toBe('number');
});
});

it('criticalUnresolved equals severityBreakdown.critical', () => {
const report = service.getSecurityReport();
expect(report.criticalUnresolved).toBe(report.severityBreakdown.critical);
});
});
});
46 changes: 46 additions & 0 deletions apps/backend/src/modules/reporting/reporting.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable } from '@nestjs/common';
import {
SecurityReport,
SeverityBreakdown,
ChainBreakdown,
} from './interfaces/reporting.interface';

/**
* Aggregates alert data into executive-level security reports.
* In production this would query a database; the static data here
* is the minimum viable implementation for the reporting interface.
*/
@Injectable()
export class ReportingService {
/**
* Returns a security summary for the given number of past days.
* @param periodDays - Look-back window in days (default: 30)
*/
getSecurityReport(periodDays = 30): SecurityReport {
const severityBreakdown: SeverityBreakdown = {
low: 12,
medium: 8,
high: 5,
critical: 2,
};

const totalAlerts = Object.values(severityBreakdown).reduce((a, b) => a + b, 0);

const topChains: ChainBreakdown[] = [
{ chain: 'Ethereum', count: 11 },
{ chain: 'Soroban', count: 9 },
{ chain: 'Polygon', count: 7 },
];

return {
generatedAt: new Date().toISOString(),
periodDays,
totalAlerts,
severityBreakdown,
topChains,
resolvedAlerts: 20,
unresolvedAlerts: totalAlerts - 20,
criticalUnresolved: severityBreakdown.critical,
};
}
}
47 changes: 29 additions & 18 deletions apps/dashboard/src/components/AlertHistoryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,34 +58,47 @@ export const AlertHistoryTable: React.FC = () => {
const uniqueChains = ['All', ...Array.from(new Set(mockEvents.map(event => event.chain)))];

// Filter events based on selected chain
const filteredEvents = filterChain === 'All'
? mockEvents
: mockEvents.filter(event => event.chain === filterChain);
const filteredEvents =
filterChain === 'All' ? mockEvents : mockEvents.filter(event => event.chain === filterChain);

return (
<div className="alert-history-container">
<div className="alert-history-card">
<div className="card-header">
<h2 className="card-title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.5 12H16c-.7 2-2 3-4 3s-3.3-1-4-3H2.5"/>
<path d="M5.5 5.5A5 5 0 0 1 9 4h6a5 5 0 0 1 3.5 1.5"/>
<path d="M11 2v2"/>
<path d="M13 2v2"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21.5 12H16c-.7 2-2 3-4 3s-3.3-1-4-3H2.5" />
<path d="M5.5 5.5A5 5 0 0 1 9 4h6a5 5 0 0 1 3.5 1.5" />
<path d="M11 2v2" />
<path d="M13 2v2" />
</svg>
Alert History
</h2>

<div className="filter-wrapper">
<label htmlFor="chain-filter" className="filter-label">Filter by Chain</label>
<select
id="chain-filter"
<label htmlFor="chain-filter" className="filter-label">
Filter by Chain
</label>
<select
id="chain-filter"
className="chain-select"
value={filterChain}
onChange={(e) => setFilterChain(e.target.value)}
onChange={e => setFilterChain(e.target.value)}
>
{uniqueChains.map(chain => (
<option key={chain} value={chain}>{chain}</option>
<option key={chain} value={chain}>
{chain}
</option>
))}
</select>
</div>
Expand All @@ -103,16 +116,14 @@ export const AlertHistoryTable: React.FC = () => {
</thead>
<tbody>
{filteredEvents.length > 0 ? (
filteredEvents.map((event) => (
filteredEvents.map(event => (
<tr key={event.id}>
<td className="timestamp-cell">{event.timestamp}</td>
<td>
<span className="chain-badge">{event.chain}</span>
</td>
<td>
<span className={`severity-tag ${event.severity}`}>
{event.severity}
</span>
<span className={`severity-tag ${event.severity}`}>{event.severity}</span>
</td>
<td className="description-cell" title={event.description}>
{event.description}
Expand Down
Loading
Loading