diff --git a/src/modules/security-posture/interfaces/posture-factor.interface.ts b/src/modules/security-posture/interfaces/posture-factor.interface.ts new file mode 100644 index 0000000..f2057a1 --- /dev/null +++ b/src/modules/security-posture/interfaces/posture-factor.interface.ts @@ -0,0 +1,28 @@ +export interface ScoringFactorConfig { + factorId: string; + name: string; + weight: number; // Relative importance multiplier + maxContribution: number; // Caps the total deduction value for this metric + enabled: boolean; +} + +export interface FactorInputMetrics { + factorId: string; + rawCount: number; // Total alerts or failures tracked + severityMultiplier: number; +} + +export interface FactorResult { + factorId: string; + name: string; + rawDeduction: number; + weightedDeduction: number; +} + +export interface PostureScoreBreakdown { + organizationId: string; + timestamp: Date; + overallScore: number; + factors: FactorResult[]; + calculationVersion: string; +} diff --git a/src/modules/security-posture/posture-score.engine.ts b/src/modules/security-posture/posture-score.engine.ts new file mode 100644 index 0000000..b27659d --- /dev/null +++ b/src/modules/security-posture/posture-score.engine.ts @@ -0,0 +1,69 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { + ScoringFactorConfig, + FactorInputMetrics, + PostureScoreBreakdown, + FactorResult, +} from './interfaces/posture-factor.interface'; + +@Injectable() +export class PostureScoreEngine { + private readonly logger = new Logger(PostureScoreEngine.name); + private readonly ENGINE_VERSION = '1.0.0'; + + /** + * Calculates a normalized 0-100 score given inputs and configurations. + * Logic handles missing telemetry gracefully without failing the entire cycle. + */ + public calculateScore( + organizationId: string, + configs: ScoringFactorConfig[], + metrics: FactorInputMetrics[], + ): PostureScoreBreakdown { + let totalDeductions = 0; + const factorBreakdowns: FactorResult[] = []; + + // Map metrics for fast lookup + const metricMap = new Map(metrics.map(m => [m.factorId, m])); + + for (const config of configs) { + if (!config.enabled) continue; + + try { + const metric = metricMap.get(config.factorId) || { rawCount: 0, severityMultiplier: 1 }; + + // Deduction formula: raw occurrences multiplied by severity impact + const rawDeduction = metric.rawCount * metric.severityMultiplier; + + // Apply weight configuration and enforce the maximum contribution cap + const weightedDeduction = Math.min(rawDeduction * config.weight, config.maxContribution); + + totalDeductions += weightedDeduction; + + factorBreakdowns.push({ + factorId: config.factorId, + name: config.name, + rawDeduction, + weightedDeduction, + }); + } catch (error) { + this.logger.error( + `Failed to process score factor [${config.factorId}] for org ${organizationId}:`, + error, + ); + // Fault isolation: continue tracking remaining metrics + } + } + + // Normalize final output strictly between 0 and 100 + const overallScore = Math.max(0, Math.min(100, 100 - totalDeductions)); + + return { + organizationId, + timestamp: new Date(), + overallScore: Math.round(overallScore * 100) / 100, // Round to two decimal spots + factors: factorBreakdowns, + calculationVersion: this.ENGINE_VERSION, + }; + } +} diff --git a/src/modules/security-posture/security-posture.service.ts b/src/modules/security-posture/security-posture.service.ts new file mode 100644 index 0000000..f182352 --- /dev/null +++ b/src/modules/security-posture/security-posture.service.ts @@ -0,0 +1,45 @@ +// src/modules/security-posture/security-posture.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { PostureScoreEngine } from './posture-score.engine'; +import { + PostureScoreBreakdown, + ScoringFactorConfig, + FactorInputMetrics, +} from './interfaces/posture-factor.interface'; + +@Injectable() +export class SecurityPostureService { + private readonly logger = new Logger(SecurityPostureService.name); + private readonly ALERT_THRESHOLD = 70; // Triggers notifications when score falls below 70 + + constructor( + private readonly scoreEngine: PostureScoreEngine, + // Inject prisma or repository reference here + ) {} + + public async evaluateOrganizationPosture( + organizationId: string, + factorConfigs: ScoringFactorConfig[], + telemetryData: FactorInputMetrics[], + ): Promise { + // 1. Compute score breakdown using the deterministic calculation engine + const breakdown = this.scoreEngine.calculateScore(organizationId, factorConfigs, telemetryData); + + // 2. Alert integration verification check + if (breakdown.overallScore < this.ALERT_THRESHOLD) { + this.triggerScoreDegradationAlert(organizationId, breakdown.overallScore); + } + + this.logger.log( + `Successfully completed security posture processing for Org: ${organizationId}. Score: ${breakdown.overallScore}`, + ); + return breakdown; + } + + private triggerScoreDegradationAlert(organizationId: string, score: number): void { + this.logger.warn( + `ALERT: Organization [${organizationId}] security posture dropped to ${score}. Threshold breached.`, + ); + // Integration logic for notifying operations or dispatching an alert entry goes here + } +}