// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
import { createLogger } from '../../common/logger.js';
const logger = createLogger({ component: 'fedramp-metrics' });
// =============================================================================
// TYPES
// =============================================================================
/**
 * Time periods for metrics
 */
export const TIME_PERIODS = ['daily', 'weekly', 'monthly', 'quarterly', 'annually'];
/**
 * Metric trend direction
 */
export const TREND_DIRECTIONS = ['improving', 'stable', 'degrading'];
// =============================================================================
// METRICS SERVICE
// =============================================================================
/**
 * FedRAMP Metrics Service
 */
export class MetricsService {
    config;
    vulnerabilityHistory;
    poamHistory;
    controlHistory;
    scanHistory;
    constructor(config) {
        this.config = config;
        this.vulnerabilityHistory = [];
        this.poamHistory = [];
        this.controlHistory = [];
        this.scanHistory = [];
        logger.info({ systemName: config.systemName }, 'Metrics service initialized');
    }
    // ===========================================================================
    // VULNERABILITY AGING METRICS
    // ===========================================================================
    /**
     * Calculate vulnerability aging metrics
     */
    calculateVulnerabilityMetrics(vulnerabilities) {
        const openVulns = vulnerabilities.filter((v) => ['open', 'in-progress'].includes(v.status));
        const now = new Date();
        const bySeverity = {
            critical: this.calculateSeverityMetrics(openVulns, 'critical', now),
            high: this.calculateSeverityMetrics(openVulns, 'high', now),
            moderate: this.calculateSeverityMetrics(openVulns, 'moderate', now),
            low: this.calculateSeverityMetrics(openVulns, 'low', now),
        };
        const agingBuckets = this.calculateAgingBuckets(openVulns, now);
        const allAges = openVulns.map((v) => Math.floor((now.getTime() - v.firstDetected.getTime()) / (1000 * 60 * 60 * 24)));
        const overallAverageAgeDays = allAges.length > 0 ? Math.round(allAges.reduce((a, b) => a + b, 0) / allAges.length) : 0;
        const oldestVulnerabilityDays = allAges.length > 0 ? Math.max(...allAges) : 0;
        // Calculate trend
        const previousAverage = this.vulnerabilityHistory.length > 0
            ? this.vulnerabilityHistory[this.vulnerabilityHistory.length - 1].value
            : undefined;
        const trend = this.calculateTrend(overallAverageAgeDays, previousAverage, true);
        const metrics = {
            calculatedAt: now,
            totalOpen: openVulns.length,
            bySeverity,
            agingBuckets,
            overallAverageAgeDays,
            oldestVulnerabilityDays,
            trend,
            previousPeriodAverage: previousAverage,
        };
        // Store historical data point
        if (this.config.trackHistory) {
            this.vulnerabilityHistory.push({
                timestamp: now,
                value: overallAverageAgeDays,
                metadata: { totalOpen: openVulns.length },
            });
        }
        return metrics;
    }
    calculateSeverityMetrics(vulns, severity, now) {
        const severityVulns = vulns.filter((v) => v.severity === severity);
        const sla = this.config.remediationSLAs[severity];
        const ages = severityVulns.map((v) => Math.floor((now.getTime() - v.firstDetected.getTime()) / (1000 * 60 * 60 * 24)));
        const withinSLA = severityVulns.filter((v) => {
            const age = Math.floor((now.getTime() - v.firstDetected.getTime()) / (1000 * 60 * 60 * 24));
            return age <= sla;
        }).length;
        return {
            total: severityVulns.length,
            averageAgeDays: ages.length > 0 ? Math.round(ages.reduce((a, b) => a + b, 0) / ages.length) : 0,
            oldestDays: ages.length > 0 ? Math.max(...ages) : 0,
            withinSLA,
            outsideSLA: severityVulns.length - withinSLA,
            percentWithinSLA: severityVulns.length > 0 ? Math.round((withinSLA / severityVulns.length) * 100) : 100,
        };
    }
    calculateAgingBuckets(vulns, now) {
        const buckets = { '0-30': 0, '31-60': 0, '61-90': 0, '91-180': 0, '180+': 0 };
        for (const vuln of vulns) {
            const age = Math.floor((now.getTime() - vuln.firstDetected.getTime()) / (1000 * 60 * 60 * 24));
            if (age <= 30)
                buckets['0-30']++;
            else if (age <= 60)
                buckets['31-60']++;
            else if (age <= 90)
                buckets['61-90']++;
            else if (age <= 180)
                buckets['91-180']++;
            else
                buckets['180+']++;
        }
        return buckets;
    }
    // ===========================================================================
    // POA&M METRICS
    // ===========================================================================
    /**
     * Calculate POA&M metrics
     */
    calculatePOAMMetrics(poamItems) {
        const now = new Date();
        const byStatus = {
            open: poamItems.filter((i) => i.status === 'open').length,
            ongoing: poamItems.filter((i) => i.status === 'ongoing').length,
            completed: poamItems.filter((i) => i.status === 'completed').length,
            closed: poamItems.filter((i) => i.status === 'closed').length,
            'risk-accepted': poamItems.filter((i) => i.status === 'risk-accepted').length,
            'false-positive': poamItems.filter((i) => i.status === 'false-positive').length,
            delayed: poamItems.filter((i) => i.status === 'delayed').length,
        };
        const openItems = poamItems.filter((i) => ['open', 'ongoing', 'delayed'].includes(i.status));
        const overdueItems = openItems.filter((i) => i.daysOverdue > 0);
        const byRiskLevel = this.calculatePOAMRiskLevelMetrics(poamItems, now);
        // Calculate closure rate
        const ninetyDaysAgo = new Date();
        ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
        const closedLast90Days = poamItems.filter((i) => i.status === 'closed' && i.actualCompletionDate && i.actualCompletionDate >= ninetyDaysAgo);
        const openedLast90Days = poamItems.filter((i) => i.detectedDate >= ninetyDaysAgo);
        const closureRate = openedLast90Days.length > 0
            ? Math.round((closedLast90Days.length / openedLast90Days.length) * 100)
            : 100;
        // Calculate average time to close
        const closedItems = poamItems.filter((i) => i.status === 'closed' && i.actualCompletionDate);
        const avgTimeToClose = closedItems.length > 0
            ? Math.round(closedItems.reduce((sum, i) => {
                return sum + ((i.actualCompletionDate.getTime() - i.detectedDate.getTime()) / (1000 * 60 * 60 * 24));
            }, 0) / closedItems.length)
            : 0;
        // Last 30 days
        const thirtyDaysAgo = new Date();
        thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
        const openedLast30 = poamItems.filter((i) => i.detectedDate >= thirtyDaysAgo).length;
        const closedLast30 = poamItems.filter((i) => i.status === 'closed' && i.actualCompletionDate && i.actualCompletionDate >= thirtyDaysAgo).length;
        const previousTotal = this.poamHistory.length > 0
            ? this.poamHistory[this.poamHistory.length - 1].value
            : undefined;
        const trend = this.calculateTrend(openItems.length, previousTotal, true);
        const metrics = {
            calculatedAt: now,
            totalItems: poamItems.length,
            byStatus,
            byRiskLevel,
            overdueItems: overdueItems.length,
            overduePercentage: openItems.length > 0 ? Math.round((overdueItems.length / openItems.length) * 100) : 0,
            averageTimeToCloseDays: avgTimeToClose,
            closureRateLast90Days: closureRate,
            openedLast30Days: openedLast30,
            closedLast30Days: closedLast30,
            netChangeLast30Days: openedLast30 - closedLast30,
            trend,
            deviationsPending: poamItems.filter((i) => i.deviationRequestId).length,
            deviationsApproved: poamItems.filter((i) => i.status === 'risk-accepted').length,
        };
        if (this.config.trackHistory) {
            this.poamHistory.push({
                timestamp: now,
                value: openItems.length,
                metadata: { overdueItems: overdueItems.length },
            });
        }
        return metrics;
    }
    calculatePOAMRiskLevelMetrics(items, now) {
        const riskLevels = ['very-high', 'high', 'moderate', 'low', 'very-low'];
        const result = {};
        for (const level of riskLevels) {
            const levelItems = items.filter((i) => i.riskLevel === level);
            const openItems = levelItems.filter((i) => ['open', 'ongoing', 'delayed'].includes(i.status));
            const overdueItems = openItems.filter((i) => i.daysOverdue > 0);
            const ages = openItems.map((i) => Math.floor((now.getTime() - i.detectedDate.getTime()) / (1000 * 60 * 60 * 24)));
            result[level] = {
                total: levelItems.length,
                open: openItems.length,
                overdue: overdueItems.length,
                averageAgeDays: ages.length > 0 ? Math.round(ages.reduce((a, b) => a + b, 0) / ages.length) : 0,
            };
        }
        return result;
    }
    // ===========================================================================
    // CONTROL IMPLEMENTATION METRICS
    // ===========================================================================
    /**
     * Calculate control implementation metrics
     */
    calculateControlMetrics(controls) {
        const now = new Date();
        const byStatus = {
            implemented: controls.filter((c) => c.implementation === 'implemented').length,
            'partially-implemented': controls.filter((c) => c.implementation === 'partially-implemented').length,
            planned: controls.filter((c) => c.implementation === 'planned').length,
            'not-applicable': controls.filter((c) => c.implementation === 'not-applicable').length,
        };
        const applicableControls = controls.filter((c) => c.implementation !== 'not-applicable');
        const implementedControls = controls.filter((c) => c.implementation === 'implemented');
        const partialControls = controls.filter((c) => c.implementation === 'partially-implemented');
        const implementationPercentage = applicableControls.length > 0
            ? Math.round((implementedControls.length / applicableControls.length) * 100)
            : 0;
        const partialPercentage = applicableControls.length > 0
            ? Math.round((partialControls.length / applicableControls.length) * 100)
            : 0;
        const byFamily = this.calculateFamilyMetrics(controls);
        const controlsWithTests = controls.filter((c) => c.assessmentProcedure.automatedTestAvailable);
        const automatedTestingCoverage = {
            total: controls.length,
            withAutomatedTests: controlsWithTests.length,
            percentage: Math.round((controlsWithTests.length / controls.length) * 100),
        };
        const p1Controls = controls.filter((c) => c.priority === 'P1');
        const p1Implemented = p1Controls.filter((c) => c.implementation === 'implemented');
        const p1Implementation = {
            total: p1Controls.length,
            implemented: p1Implemented.length,
            percentage: p1Controls.length > 0 ? Math.round((p1Implemented.length / p1Controls.length) * 100) : 0,
        };
        const previousPercentage = this.controlHistory.length > 0
            ? this.controlHistory[this.controlHistory.length - 1].value
            : undefined;
        const trend = this.calculateTrend(implementationPercentage, previousPercentage, false);
        const metrics = {
            calculatedAt: now,
            totalControls: controls.length,
            byStatus,
            implementationPercentage,
            partialImplementationPercentage: partialPercentage,
            byFamily,
            automatedTestingCoverage,
            p1Implementation,
            trend,
            previousPeriodPercentage: previousPercentage,
        };
        if (this.config.trackHistory) {
            this.controlHistory.push({
                timestamp: now,
                value: implementationPercentage,
            });
        }
        return metrics;
    }
    calculateFamilyMetrics(controls) {
        const families = new Map();
        for (const control of controls) {
            const family = control.family;
            const existing = families.get(family) || [];
            existing.push(control);
            families.set(family, existing);
        }
        return Array.from(families.entries())
            .map(([family, familyControls]) => ({
            family,
            total: familyControls.length,
            implemented: familyControls.filter((c) => c.implementation === 'implemented').length,
            percentage: Math.round((familyControls.filter((c) => c.implementation === 'implemented').length /
                familyControls.length) *
                100),
        }))
            .sort((a, b) => a.family.localeCompare(b.family));
    }
    // ===========================================================================
    // SCAN COMPLIANCE METRICS
    // ===========================================================================
    /**
     * Calculate scan compliance metrics
     */
    calculateScanMetrics(scans, totalAssets, scannedAssetIds) {
        const now = new Date();
        // Monthly scan compliance for last 12 months
        const monthlyCompliance = [];
        const missedMonths = [];
        for (let i = 11; i >= 0; i--) {
            const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
            const monthEnd = new Date(now.getFullYear(), now.getMonth() - i + 1, 0);
            const monthName = monthStart.toLocaleString('default', { month: 'short', year: 'numeric' });
            const monthScans = scans.filter((s) => s.endTime >= monthStart && s.endTime <= monthEnd && s.scanType === 'vulnerability');
            if (monthScans.length > 0) {
                monthlyCompliance.push(1);
            }
            else {
                monthlyCompliance.push(0);
                missedMonths.push(monthName);
            }
        }
        const percentCompliant = Math.round((monthlyCompliance.filter((m) => m === 1).length / 12) * 100);
        // Last scan info
        const lastScan = scans
            .filter((s) => s.scanType === 'vulnerability')
            .sort((a, b) => b.endTime.getTime() - a.endTime.getTime())[0];
        const daysSinceLastScan = lastScan
            ? Math.floor((now.getTime() - lastScan.endTime.getTime()) / (1000 * 60 * 60 * 24))
            : undefined;
        // Coverage
        const unscannedAssets = Array.from({ length: totalAssets }, (_, i) => `asset-${i}`).filter((id) => !scannedAssetIds.includes(id));
        // Findings trends (last 6 months)
        const findingsTrends = this.calculateFindingsTrends(scans);
        // Authenticated scan percentage
        const authenticatedScans = scans.filter((s) => s.credentialType);
        const authenticatedPercentage = scans.length > 0
            ? Math.round((authenticatedScans.length / scans.length) * 100)
            : 0;
        const previousCompliance = this.scanHistory.length > 0
            ? this.scanHistory[this.scanHistory.length - 1].value
            : undefined;
        const trend = this.calculateTrend(percentCompliant, previousCompliance, false);
        const metrics = {
            calculatedAt: now,
            monthlyScanCompliance: {
                lastTwelveMonths: monthlyCompliance,
                percentCompliant,
                missedMonths,
            },
            lastScan: {
                date: lastScan?.endTime,
                type: lastScan?.scanType,
                daysSinceLastScan,
                isOverdue: daysSinceLastScan !== undefined && daysSinceLastScan > 30,
            },
            coverage: {
                totalAssets,
                scannedAssets: scannedAssetIds.length,
                coveragePercentage: Math.round((scannedAssetIds.length / totalAssets) * 100),
                unscannedAssets: unscannedAssets.slice(0, 10), // Limit for display
            },
            findingsTrends,
            authenticatedScanPercentage: authenticatedPercentage,
            trend,
        };
        if (this.config.trackHistory) {
            this.scanHistory.push({
                timestamp: now,
                value: percentCompliant,
            });
        }
        return metrics;
    }
    calculateFindingsTrends(scans) {
        const trends = [];
        const now = new Date();
        for (let i = 5; i >= 0; i--) {
            const monthStart = new Date(now.getFullYear(), now.getMonth() - i, 1);
            const monthEnd = new Date(now.getFullYear(), now.getMonth() - i + 1, 0);
            const monthName = monthStart.toLocaleString('default', { month: 'short' });
            const monthScans = scans.filter((s) => s.endTime >= monthStart && s.endTime <= monthEnd);
            const totals = { critical: 0, high: 0, moderate: 0, low: 0 };
            for (const scan of monthScans) {
                totals.critical += scan.findingCounts.critical;
                totals.high += scan.findingCounts.high;
                totals.moderate += scan.findingCounts.moderate;
                totals.low += scan.findingCounts.low;
            }
            trends.push({
                period: monthName,
                ...totals,
            });
        }
        return trends;
    }
    // ===========================================================================
    // DASHBOARD METRICS
    // ===========================================================================
    /**
     * Generate comprehensive ConMon dashboard metrics
     */
    generateDashboardMetrics(vulnerabilities, poamItems, controls, scans, totalAssets, scannedAssetIds) {
        const now = new Date();
        const vulnMetrics = this.calculateVulnerabilityMetrics(vulnerabilities);
        const poamMetrics = this.calculatePOAMMetrics(poamItems);
        const controlMetrics = this.calculateControlMetrics(controls);
        const scanMetrics = this.calculateScanMetrics(scans, totalAssets, scannedAssetIds);
        // Calculate days until ATO expiration
        const daysUntilExpiration = Math.floor((this.config.atoExpirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
        let authorizationStatus;
        if (daysUntilExpiration < 0) {
            authorizationStatus = 'expired';
        }
        else if (daysUntilExpiration < 90) {
            authorizationStatus = 'expiring-soon';
        }
        else {
            authorizationStatus = 'active';
        }
        // Calculate overall compliance score
        const overallScore = this.calculateOverallScore(vulnMetrics, poamMetrics, controlMetrics, scanMetrics);
        // Key risk indicators
        const kris = this.calculateKeyRiskIndicators(vulnMetrics, poamMetrics, controlMetrics, scanMetrics);
        // Action items
        const actionItems = this.generateActionItems(vulnMetrics, poamMetrics, controlMetrics, scanMetrics, authorizationStatus, daysUntilExpiration);
        return {
            generatedAt: now,
            systemName: this.config.systemName,
            authorizationStatus,
            daysUntilATOExpiration: daysUntilExpiration,
            overallComplianceScore: overallScore,
            vulnerabilityMetrics: vulnMetrics,
            poamMetrics,
            controlMetrics,
            scanMetrics,
            keyRiskIndicators: kris,
            actionItems,
        };
    }
    calculateOverallScore(vulnMetrics, poamMetrics, controlMetrics, scanMetrics) {
        // Weighted score calculation
        const vulnScore = 100 - Math.min(vulnMetrics.totalOpen * 2, 50);
        const poamScore = 100 - poamMetrics.overduePercentage;
        const controlScore = controlMetrics.implementationPercentage;
        const scanScore = scanMetrics.monthlyScanCompliance.percentCompliant;
        // Weights: Vulnerabilities 30%, POA&M 25%, Controls 25%, Scans 20%
        return Math.round(vulnScore * 0.3 + poamScore * 0.25 + controlScore * 0.25 + scanScore * 0.2);
    }
    calculateKeyRiskIndicators(vulnMetrics, poamMetrics, controlMetrics, scanMetrics) {
        return [
            {
                indicator: 'Critical Vulnerabilities',
                value: vulnMetrics.bySeverity.critical.total,
                threshold: 0,
                status: vulnMetrics.bySeverity.critical.total === 0 ? 'green' : 'red',
                trend: vulnMetrics.trend,
            },
            {
                indicator: 'High Vulnerabilities',
                value: vulnMetrics.bySeverity.high.total,
                threshold: 5,
                status: vulnMetrics.bySeverity.high.total <= 5 ? 'green' : vulnMetrics.bySeverity.high.total <= 10 ? 'yellow' : 'red',
                trend: vulnMetrics.trend,
            },
            {
                indicator: 'Overdue POA&M Items',
                value: poamMetrics.overdueItems,
                threshold: 0,
                status: poamMetrics.overdueItems === 0 ? 'green' : poamMetrics.overdueItems <= 3 ? 'yellow' : 'red',
                trend: poamMetrics.trend,
            },
            {
                indicator: 'Control Implementation',
                value: controlMetrics.implementationPercentage,
                threshold: 100,
                status: controlMetrics.implementationPercentage >= 95 ? 'green' : controlMetrics.implementationPercentage >= 80 ? 'yellow' : 'red',
                trend: controlMetrics.trend,
            },
            {
                indicator: 'Scan Compliance',
                value: scanMetrics.monthlyScanCompliance.percentCompliant,
                threshold: 100,
                status: scanMetrics.monthlyScanCompliance.percentCompliant === 100 ? 'green' : scanMetrics.monthlyScanCompliance.percentCompliant >= 90 ? 'yellow' : 'red',
                trend: scanMetrics.trend,
            },
        ];
    }
    generateActionItems(vulnMetrics, poamMetrics, controlMetrics, scanMetrics, authStatus, daysToExpiration) {
        const items = [];
        if (vulnMetrics.bySeverity.critical.total > 0) {
            items.push({
                priority: 'critical',
                description: `Remediate ${vulnMetrics.bySeverity.critical.total} critical vulnerabilities immediately`,
                dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
            });
        }
        if (poamMetrics.overdueItems > 0) {
            items.push({
                priority: 'high',
                description: `Address ${poamMetrics.overdueItems} overdue POA&M items`,
            });
        }
        if (scanMetrics.lastScan.isOverdue) {
            items.push({
                priority: 'high',
                description: 'Conduct monthly vulnerability scan - scan is overdue',
            });
        }
        if (authStatus === 'expiring-soon') {
            items.push({
                priority: 'high',
                description: `ATO expires in ${daysToExpiration} days - initiate reauthorization`,
            });
        }
        if (controlMetrics.implementationPercentage < 100) {
            items.push({
                priority: 'medium',
                description: `Complete implementation of ${controlMetrics.byStatus.planned} planned controls`,
            });
        }
        return items.sort((a, b) => {
            const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
            return priorityOrder[a.priority] - priorityOrder[b.priority];
        });
    }
    // ===========================================================================
    // UTILITY FUNCTIONS
    // ===========================================================================
    calculateTrend(current, previous, lowerIsBetter) {
        if (previous === undefined)
            return 'stable';
        const threshold = 5; // 5% change threshold
        const change = ((current - previous) / previous) * 100;
        if (Math.abs(change) < threshold)
            return 'stable';
        if (lowerIsBetter) {
            return change < 0 ? 'improving' : 'degrading';
        }
        else {
            return change > 0 ? 'improving' : 'degrading';
        }
    }
    /**
     * Get historical data for a metric
     */
    getHistory(metricType) {
        switch (metricType) {
            case 'vulnerability':
                return [...this.vulnerabilityHistory];
            case 'poam':
                return [...this.poamHistory];
            case 'control':
                return [...this.controlHistory];
            case 'scan':
                return [...this.scanHistory];
        }
    }
    /**
     * Export metrics to JSON
     */
    exportMetrics(dashboard) {
        return JSON.stringify(dashboard, null, 2);
    }
}
// =============================================================================
// EXPORTS
// =============================================================================
export default MetricsService;
//# sourceMappingURL=metrics.js.map