diff --git a/src/modules/security-posture/enums/asset-risk.enum.ts b/src/modules/security-posture/enums/asset-risk.enum.ts new file mode 100644 index 0000000..87700d8 --- /dev/null +++ b/src/modules/security-posture/enums/asset-risk.enum.ts @@ -0,0 +1,6 @@ +export enum AssetRiskSeverity { + LOW = 'LOW', + MEDIUM = 'MEDIUM', + HIGH = 'HIGH', + CRITICAL = 'CRITICAL', +} diff --git a/src/modules/security-posture/factor-calculator.service.ts b/src/modules/security-posture/factor-calculator.service.ts new file mode 100644 index 0000000..ded4831 --- /dev/null +++ b/src/modules/security-posture/factor-calculator.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { AssetDrainDetector } from './utils/asset-drain.detector'; +import { AssetTransferEvent } from './interfaces/asset-drain.interface'; +import { FactorInputMetrics } from './interfaces/posture-factor.interface'; + +@Injectable() +export class FactorCalculatorService { + constructor(private readonly drainDetector: AssetDrainDetector) {} + + /** + * Resolves transfer streams into unified factor metrics readable by the scoring engine + */ + public compileAssetDrainMetrics( + transfers: AssetTransferEvent[], + currentTreasuryBalance: number, + ): FactorInputMetrics { + const assessment = this.drainDetector.evaluateOutflows(transfers, currentTreasuryBalance); + + return { + factorId: 'rapid_asset_drain', + rawCount: assessment.isDraining ? 1 : 0, + severityMultiplier: assessment.deductionScore, // Directly injects dynamic deduction severity + }; + } +} diff --git a/src/modules/security-posture/interfaces/asset-drain.interface.ts b/src/modules/security-posture/interfaces/asset-drain.interface.ts new file mode 100644 index 0000000..bbd9821 --- /dev/null +++ b/src/modules/security-posture/interfaces/asset-drain.interface.ts @@ -0,0 +1,18 @@ +// src/modules/security-posture/interfaces/asset-drain.interface.ts +import { AssetRiskSeverity } from '../enums/asset-risk.enum'; + +export interface AssetTransferEvent { + transactionHash: string; + tokenAddress: string; + amount: number; // Normalized decimal value + usdValue: number; // Value at time of execution + timestamp: Date; +} + +export interface DrainageEvaluationResult { + isDraining: boolean; + severity: AssetRiskSeverity; + totalValueLostUsd: number; + percentageDrop: number; + deductionScore: number; +} diff --git a/src/modules/security-posture/utils/asset-drain.detector.ts b/src/modules/security-posture/utils/asset-drain.detector.ts new file mode 100644 index 0000000..569c747 --- /dev/null +++ b/src/modules/security-posture/utils/asset-drain.detector.ts @@ -0,0 +1,59 @@ +// src/modules/security-posture/utils/asset-drain.detector.ts +import { Injectable } from '@nestjs/common'; +import { AssetRiskSeverity } from '../enums/asset-risk.enum'; +import { AssetTransferEvent, DrainageEvaluationResult } from '../interfaces/asset-drain.interface'; + +@Injectable() +export class AssetDrainDetector { + // Configurations for rapid draining thresholds + private readonly DETECTION_WINDOW_MS = 5 * 60 * 1000; // 5-minute rolling window + private readonly CRITICAL_VALUE_THRESHOLD_USD = 50000; // $50k outflow drops within window triggers alarm + + /** + * Processes transfer events deterministically to identify rapid draining anomalies. + */ + public evaluateOutflows( + transfers: AssetTransferEvent[], + startingTreasuryValueUsd: number, + ): DrainageEvaluationResult { + const now = Date.now(); + const activeWindowStart = now - this.DETECTION_WINDOW_MS; + + // Filter outflows that occurred strictly within the active evaluation window + const recentTransfers = transfers.filter( + t => new Date(t.timestamp).getTime() >= activeWindowStart, + ); + + const totalValueLostUsd = recentTransfers.reduce((sum, t) => sum + t.usdValue, 0); + const percentageDrop = + startingTreasuryValueUsd > 0 ? (totalValueLostUsd / startingTreasuryValueUsd) * 100 : 0; + + // Trigger classification thresholds + const isDraining = + totalValueLostUsd >= this.CRITICAL_VALUE_THRESHOLD_USD || percentageDrop >= 20; + + let severity = AssetRiskSeverity.LOW; + let deductionScore = 0; + + if (isDraining) { + if (percentageDrop >= 50 || totalValueLostUsd >= 250000) { + severity = AssetRiskSeverity.CRITICAL; + deductionScore = 40; // Max penalty contribution to security posture + } else { + severity = AssetRiskSeverity.HIGH; + deductionScore = 25; + } + } else if (totalValueLostUsd > this.CRITICAL_VALUE_THRESHOLD_USD * 0.5) { + severity = AssetRiskSeverity.MEDIUM; + deductionScore = 10; + } + + return { + isDraining, + severity, + totalValueLostUsd, + percentageDrop: Math.round(percentageDrop * 100) / 100, + deductionScore, + }; + } +}