From 1fd4fced7772693b560e84cc61e1091c131e4a93 Mon Sep 17 00:00:00 2001 From: Muhammad Mustapha Date: Wed, 17 Jun 2026 17:22:39 +0100 Subject: [PATCH 1/5] feat: implement risk trends dashboard with charts and comparisons --- apps/web/app/risk-trends/page.tsx | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 apps/web/app/risk-trends/page.tsx diff --git a/apps/web/app/risk-trends/page.tsx b/apps/web/app/risk-trends/page.tsx new file mode 100644 index 0000000..955d1f9 --- /dev/null +++ b/apps/web/app/risk-trends/page.tsx @@ -0,0 +1,92 @@ +"use client"; + +import React, { useState } from 'react'; + +export default function RiskTrendsDashboard() { + const [timeFilter, setTimeFilter] = useState('30d'); + + // Mock data for charts + const historicalData = [ + { date: '2023-01-01', riskScore: 85 }, + { date: '2023-02-01', riskScore: 70 }, + { date: '2023-03-01', riskScore: 65 }, + { date: '2023-04-01', riskScore: 50 }, + { date: '2023-05-01', riskScore: 55 }, + { date: '2023-06-01', riskScore: 40 }, + ]; + + return ( +
+

+ Risk Trends Dashboard +

+ + {/* Time Filters */} +
+ + +
+ +
+ {/* Historical Charts */} +
+

Historical Risk Chart

+
+ {historicalData.map((data, i) => ( +
+
+
60 ? '#ef4444' : data.riskScore > 40 ? '#eab308' : '#22c55e', + height: `${data.riskScore}%`, + borderRadius: '4px 4px 0 0', + transition: 'height 0.3s ease' + }} title={`Risk Score: ${data.riskScore}`} /> +
+ {new Date(data.date).toLocaleString('default', { month: 'short' })} +
+ ))} +
+
+ + {/* Risk Comparisons */} +
+

Risk Comparisons

+
+
+ Engineering +
+ 45 + ↓ 10% +
+
+
+ Sales +
+ 72 + ↑ 5% +
+
+
+ Marketing +
+ 30 + - 0% +
+
+
+
+
+
+ ); +} From 18c1fb6034d3274019ab971d294df3c36eda1d9f Mon Sep 17 00:00:00 2001 From: Awwal Mustapha Date: Thu, 18 Jun 2026 11:34:41 +0100 Subject: [PATCH 2/5] ci: download build artifacts in docker-build and add ci-status debug --- .github/workflows/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c00c79..39e8a97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -140,6 +140,10 @@ jobs: docker-build: name: Docker Build Check runs-on: ubuntu-latest + needs: build + strategy: + matrix: + node-version: [20.x, 22.x] permissions: contents: read packages: read @@ -148,6 +152,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Download build artifact for node ${{ matrix.node-version }} + uses: actions/download-artifact@v4 + with: + name: build-${{ matrix.node-version }} + path: dist + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -157,6 +167,7 @@ jobs: context: . file: ./Dockerfile push: false + load: true cache-from: type=gha cache-to: type=gha,mode=max @@ -169,6 +180,10 @@ jobs: steps: - name: Check CI status run: | + echo "needs.lint.result=${{ needs.lint.result }}" + echo "needs.build.result=${{ needs.build.result }}" + echo "needs.test.result=${{ needs.test.result }}" + echo "needs.docker-build.result=${{ needs.docker-build.result }}" if [[ "${{ needs.lint.result }}" != "success" || \ "${{ needs.build.result }}" != "success" || \ "${{ needs.test.result }}" != "success" || \ From 44736288b202bd98179a74c6ac4e766d010a3fc1 Mon Sep 17 00:00:00 2001 From: Awwal Mustapha Date: Mon, 22 Jun 2026 11:30:19 +0100 Subject: [PATCH 3/5] ci: fix artifact download include apps/web in eslint and type fixes --- .eslintrc.js | 4 ++-- .github/workflows/ci.yml | 7 ++++--- .../modules/governance/governance.service.ts | 3 ++- .../interfaces/governance.interface.ts | 3 ++- apps/web/app/risk-trends/page.tsx | 8 +++++--- apps/web/tsconfig.json | 17 +++++++++++++++++ 6 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 apps/web/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 6e9d29e..01c5f82 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { - project: ['tsconfig.json', 'apps/dashboard/tsconfig.json'], + project: ['tsconfig.json', 'apps/dashboard/tsconfig.json', 'apps/web/tsconfig.json'], tsconfigRootDir: __dirname, sourceType: 'module', }, @@ -23,7 +23,7 @@ module.exports = { overrides: [ { // Files outside all tsconfigs — lint without type-aware rules - files: ['observability/**/*.ts', 'prisma/**/*.ts', 'src/**/*.ts', 'env.d.ts'], + files: ['observability/**/*.ts', 'prisma/**/*.ts', 'src/**/*.ts', 'env.d.ts', 'apps/web/app/**/*'], parserOptions: { project: null, }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e8a97..d0662a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,8 +72,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: build-${{ matrix.node-version }} - path: dist/ - retention-days: 7 + # backend tsc emits to apps/backend/dist + path: apps/backend/dist/ + retention-days: 14 test: name: Tests @@ -156,7 +157,7 @@ jobs: uses: actions/download-artifact@v4 with: name: build-${{ matrix.node-version }} - path: dist + path: apps/backend/dist - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/apps/backend/src/modules/governance/governance.service.ts b/apps/backend/src/modules/governance/governance.service.ts index a71512f..36564cb 100644 --- a/apps/backend/src/modules/governance/governance.service.ts +++ b/apps/backend/src/modules/governance/governance.service.ts @@ -62,7 +62,8 @@ export class GovernanceService { proposalId: dto.proposalId, voter: dto.voter, transactionHash: dto.transactionHash, - metadata: dto.metadata || {}, + // Prisma expects JSON; cast unknown to any here after validation upstream + metadata: (dto.metadata as any) || {}, }, }); return event; diff --git a/apps/backend/src/modules/governance/interfaces/governance.interface.ts b/apps/backend/src/modules/governance/interfaces/governance.interface.ts index 28fc696..827bc85 100644 --- a/apps/backend/src/modules/governance/interfaces/governance.interface.ts +++ b/apps/backend/src/modules/governance/interfaces/governance.interface.ts @@ -3,7 +3,8 @@ export interface GovernanceEventDto { proposalId?: string; voter?: string; transactionHash: string; - metadata?: Record; + // Use unknown to avoid eslint no-explicit-any while still allowing arbitrary metadata + metadata?: unknown; } export interface ProposalDto { diff --git a/apps/web/app/risk-trends/page.tsx b/apps/web/app/risk-trends/page.tsx index 5010d59..7a38bab 100644 --- a/apps/web/app/risk-trends/page.tsx +++ b/apps/web/app/risk-trends/page.tsx @@ -2,11 +2,13 @@ import React, { useState } from 'react'; +type HistoricalPoint = { date: string; riskScore: number }; + export default function RiskTrendsDashboard() { - const [timeFilter, setTimeFilter] = useState('30d'); + const [timeFilter, setTimeFilter] = useState('30d'); // Mock data for charts - const historicalData = [ + const historicalData: HistoricalPoint[] = [ { date: '2023-01-01', riskScore: 85 }, { date: '2023-02-01', riskScore: 70 }, { date: '2023-03-01', riskScore: 65 }, @@ -57,7 +59,7 @@ export default function RiskTrendsDashboard() { >

Historical Risk Chart

- {historicalData.map((data, i) => ( + {historicalData.map((data: HistoricalPoint, i: number) => (
Date: Mon, 22 Jun 2026 11:39:07 +0100 Subject: [PATCH 4/5] chore(eslint): resolve merge conflict and include apps/web tsconfig --- .eslintrc.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 01c5f82..02e780b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,18 +1,17 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { + // Include root and application-level tsconfigs so ESLint can run type-aware rules project: ['tsconfig.json', 'apps/dashboard/tsconfig.json', 'apps/web/tsconfig.json'], tsconfigRootDir: __dirname, sourceType: 'module', }, plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], root: true, env: { node: true }, - ignorePatterns: ['.eslintrc.js', 'dist/**'], + // Exclude build artifacts and the dashboard runtime files + ignorePatterns: ['.eslintrc.js', 'dist/**', 'apps/dashboard/**'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', @@ -22,8 +21,8 @@ module.exports = { }, overrides: [ { - // Files outside all tsconfigs — lint without type-aware rules - files: ['observability/**/*.ts', 'prisma/**/*.ts', 'src/**/*.ts', 'env.d.ts', 'apps/web/app/**/*'], + // Files that are intentionally outside any tsconfig — lint without type-aware rules + files: ['observability/**/*.ts', 'prisma/**/*.ts', 'env.d.ts'], parserOptions: { project: null, }, From 7bcc2c0b3a902661888f61534faa4455e497b439 Mon Sep 17 00:00:00 2001 From: Awwal Mustapha Date: Mon, 22 Jun 2026 12:04:15 +0100 Subject: [PATCH 5/5] fix(ai): replace any with unknown and add runtime guards for AI provider; adjust eslint overrides --- src/modules/ai/ai.service.ts | 7 ++- .../ai/interfaces/threat-summary.interface.ts | 2 +- .../ai/providers/ai-provider.interface.ts | 2 +- src/modules/ai/providers/openai.provider.ts | 53 +++++++++++++------ 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/modules/ai/ai.service.ts b/src/modules/ai/ai.service.ts index b6c8334..dd45181 100644 --- a/src/modules/ai/ai.service.ts +++ b/src/modules/ai/ai.service.ts @@ -4,12 +4,15 @@ import { ThreatSummary, Severity } from './interfaces/threat-summary.interface'; export class AIService { constructor(private providers: AIProvider[] = []) {} - async summarize(event: any): Promise { + // The service accepts unknown external events; providers are responsible for + // narrowing/validating the shape before use. This keeps the service flexible + // while avoiding unsafe `any` usage. + async summarize(event: unknown): Promise { const results = await Promise.all(this.providers.map(p => p.analyzeThreat(event))); return results; } - async bestSummary(event: any): Promise { + async bestSummary(event: unknown): Promise { const summaries = await this.summarize(event); if (summaries.length === 0) return null; diff --git a/src/modules/ai/interfaces/threat-summary.interface.ts b/src/modules/ai/interfaces/threat-summary.interface.ts index a9a7500..ab057d8 100644 --- a/src/modules/ai/interfaces/threat-summary.interface.ts +++ b/src/modules/ai/interfaces/threat-summary.interface.ts @@ -8,5 +8,5 @@ export interface ThreatSummary { indicators?: string[]; recommendedActions?: string[]; confidence?: number; // 0..1 - raw?: any; + raw?: unknown; } diff --git a/src/modules/ai/providers/ai-provider.interface.ts b/src/modules/ai/providers/ai-provider.interface.ts index fe06e4c..a5b5911 100644 --- a/src/modules/ai/providers/ai-provider.interface.ts +++ b/src/modules/ai/providers/ai-provider.interface.ts @@ -2,6 +2,6 @@ import { ThreatSummary } from '../interfaces/threat-summary.interface'; export interface AIProvider { name: string; - analyzeThreat(event: any): Promise; + analyzeThreat(event: unknown): Promise; healthCheck?(): Promise; } diff --git a/src/modules/ai/providers/openai.provider.ts b/src/modules/ai/providers/openai.provider.ts index d5c6234..723fad5 100644 --- a/src/modules/ai/providers/openai.provider.ts +++ b/src/modules/ai/providers/openai.provider.ts @@ -5,15 +5,14 @@ export class OpenAIProvider implements AIProvider { name = 'openai'; constructor(private apiKey: string) {} - async analyzeThreat(event: any): Promise { - // Minimal, efficient implementation: lightweight local heuristic + optional remote call. - // For now provide a deterministic lightweight summary so the framework is usable without network. + async analyzeThreat(event: unknown): Promise { + // Narrow the external event safely using lightweight guards below. + const e = this.normalizeEvent(event); - const title = event.title || event.alert || 'Security event'; - const description = event.description || JSON.stringify(event).slice(0, 200); + const title = e.title ?? e.alert ?? 'Security event'; + const description = e.description ?? safeStringify(e).slice(0, 200); - // Simple heuristic severity mapping - const severity = this.heuristicSeverity(event); + const severity = this.heuristicSeverity(e); const score = this.heuristicScore(severity); return { @@ -21,7 +20,7 @@ export class OpenAIProvider implements AIProvider { description, severity, score, - indicators: this.extractIndicators(event), + indicators: this.extractIndicators(e), recommendedActions: this.suggestRemediations(severity), confidence: 0.5, raw: event, @@ -29,12 +28,21 @@ export class OpenAIProvider implements AIProvider { } async healthCheck(): Promise { - // If API key configured, assume provider can be used; otherwise still usable in local-only mode. return typeof this.apiKey === 'string' && this.apiKey.length > 0; } - private heuristicSeverity(event: any): 'low' | 'medium' | 'high' | 'critical' { - const s = (event.severity || '').toString().toLowerCase(); + // Lightweight event normalization: ensure we have an object with string keys. + private normalizeEvent(event: unknown): Record { + if (event && typeof event === 'object' && !Array.isArray(event)) + return event as Record; + return { raw: event }; + } + + private heuristicSeverity( + event: Record, + ): 'low' | 'medium' | 'high' | 'critical' { + const raw = event['severity']; + const s = (typeof raw === 'string' ? raw : String(raw || '')).toLowerCase(); if (s.includes('crit') || s === '4') return 'critical'; if (s.includes('high') || s === '3') return 'high'; if (s.includes('medium') || s === '2') return 'medium'; @@ -54,11 +62,16 @@ export class OpenAIProvider implements AIProvider { } } - private extractIndicators(event: any): string[] { + private extractIndicators(event: Record): string[] { const indicators: string[] = []; - if (event.ip) indicators.push(`ip:${event.ip}`); - if (event.user) indicators.push(`user:${event.user}`); - if (event.filename) indicators.push(`file:${event.filename}`); + const addIfString = (k: string, prefix: string) => { + const v = event[k]; + if (typeof v === 'string' && v.length > 0) indicators.push(`${prefix}:${v}`); + }; + + addIfString('ip', 'ip'); + addIfString('user', 'user'); + addIfString('filename', 'file'); return indicators; } @@ -74,3 +87,13 @@ export class OpenAIProvider implements AIProvider { return ['Monitor and gather additional context']; } } + +// Helper: safe stringify for unknown values +function safeStringify(v: unknown): string { + try { + if (typeof v === 'string') return v; + return JSON.stringify(v ?? ''); + } catch { + return String(v ?? ''); + } +}