/**
 * False Positive Tracker
 *
 * Tracks false positive rates per scanner/rule and adjusts
 * finding confidence accordingly.
 *
 * @module scanners/fp-tracker
 */
import { readFile, writeFile, mkdir } from "fs/promises";
import { join, dirname } from "path";
import { logger } from "../logger.js";
/**
 * Default tracking data
 */
function createDefaultTrackingData(projectPath) {
    return {
        version: "1.0.0",
        lastUpdated: new Date().toISOString(),
        projectPath,
        scanners: {},
        globalStats: {
            totalFindings: 0,
            totalTP: 0,
            totalFP: 0,
            totalPending: 0,
            overallAccuracy: 1.0,
        },
    };
}
/**
 * Get the FP tracking file path
 */
function getTrackingFilePath(projectPath) {
    return join(projectPath, ".vaspera", "fp-tracking.json");
}
/**
 * Load FP tracking data from disk
 */
export async function loadTrackingData(projectPath) {
    const filePath = getTrackingFilePath(projectPath);
    try {
        const content = await readFile(filePath, "utf-8");
        const data = JSON.parse(content);
        // Convert Map serialization back to Maps
        for (const scanner of Object.values(data.scanners)) {
            if (scanner.ruleMetrics && !(scanner.ruleMetrics instanceof Map)) {
                scanner.ruleMetrics = new Map(Object.entries(scanner.ruleMetrics));
            }
        }
        return data;
    }
    catch {
        return createDefaultTrackingData(projectPath);
    }
}
/**
 * Save FP tracking data to disk
 */
export async function saveTrackingData(projectPath, data) {
    const filePath = getTrackingFilePath(projectPath);
    // Ensure directory exists
    await mkdir(dirname(filePath), { recursive: true });
    // Convert Maps to objects for serialization
    const serializable = {
        ...data,
        lastUpdated: new Date().toISOString(),
        scanners: Object.fromEntries(Object.entries(data.scanners).map(([key, scanner]) => [
            key,
            {
                ...scanner,
                ruleMetrics: Object.fromEntries(scanner.ruleMetrics),
            },
        ])),
    };
    await writeFile(filePath, JSON.stringify(serializable, null, 2), "utf-8");
    logger.debug("fp_tracker.saved", { filePath });
}
/**
 * Record a new finding for tracking
 */
export async function recordFinding(projectPath, finding) {
    const data = await loadTrackingData(projectPath);
    const scannerKey = finding.scanner;
    // Initialize scanner metrics if needed
    if (!data.scanners[scannerKey]) {
        data.scanners[scannerKey] = {
            scanner: finding.scanner,
            totalRules: 0,
            totalFindings: 0,
            overallFPRate: 0,
            overallTPRate: 1,
            ruleMetrics: new Map(),
        };
    }
    const scanner = data.scanners[scannerKey];
    // Initialize rule metrics if needed
    if (!scanner.ruleMetrics.has(finding.ruleId)) {
        scanner.ruleMetrics.set(finding.ruleId, {
            scanner: finding.scanner,
            ruleId: finding.ruleId,
            totalReported: 0,
            confirmedTP: 0,
            confirmedFP: 0,
            pendingReview: 0,
            fpRate: 0,
            tpRate: 1,
            adjustmentFactor: 1,
            lastUpdated: new Date().toISOString(),
            severityDistribution: {},
        });
        scanner.totalRules++;
    }
    const rule = scanner.ruleMetrics.get(finding.ruleId);
    // Update counts
    rule.totalReported++;
    rule.pendingReview++;
    rule.lastUpdated = new Date().toISOString();
    // Update severity distribution
    rule.severityDistribution[finding.severity] =
        (rule.severityDistribution[finding.severity] || 0) + 1;
    // Update scanner totals
    scanner.totalFindings++;
    // Update global stats
    data.globalStats.totalFindings++;
    data.globalStats.totalPending++;
    await saveTrackingData(projectPath, data);
}
/**
 * Mark a finding as true positive
 */
export async function markTruePositive(projectPath, scanner, ruleId) {
    const data = await loadTrackingData(projectPath);
    const scannerData = data.scanners[scanner];
    if (!scannerData) {
        return undefined;
    }
    const rule = scannerData.ruleMetrics.get(ruleId);
    if (!rule) {
        return undefined;
    }
    // Update counts
    rule.confirmedTP++;
    if (rule.pendingReview > 0) {
        rule.pendingReview--;
    }
    // Recalculate rates
    const totalReviewed = rule.confirmedTP + rule.confirmedFP;
    rule.tpRate = totalReviewed > 0 ? rule.confirmedTP / totalReviewed : 1;
    rule.fpRate = totalReviewed > 0 ? rule.confirmedFP / totalReviewed : 0;
    rule.adjustmentFactor = calculateAdjustmentFactor(rule);
    rule.lastUpdated = new Date().toISOString();
    // Update global stats
    data.globalStats.totalTP++;
    if (data.globalStats.totalPending > 0) {
        data.globalStats.totalPending--;
    }
    updateGlobalAccuracy(data);
    // Update scanner rates
    updateScannerRates(scannerData);
    await saveTrackingData(projectPath, data);
    return rule;
}
/**
 * Mark a finding as false positive
 */
export async function markFalsePositive(projectPath, scanner, ruleId, reason) {
    const data = await loadTrackingData(projectPath);
    const scannerData = data.scanners[scanner];
    if (!scannerData) {
        return undefined;
    }
    const rule = scannerData.ruleMetrics.get(ruleId);
    if (!rule) {
        return undefined;
    }
    // Update counts
    rule.confirmedFP++;
    if (rule.pendingReview > 0) {
        rule.pendingReview--;
    }
    // Recalculate rates
    const totalReviewed = rule.confirmedTP + rule.confirmedFP;
    rule.tpRate = totalReviewed > 0 ? rule.confirmedTP / totalReviewed : 1;
    rule.fpRate = totalReviewed > 0 ? rule.confirmedFP / totalReviewed : 0;
    rule.adjustmentFactor = calculateAdjustmentFactor(rule);
    rule.lastUpdated = new Date().toISOString();
    // Update global stats
    data.globalStats.totalFP++;
    if (data.globalStats.totalPending > 0) {
        data.globalStats.totalPending--;
    }
    updateGlobalAccuracy(data);
    // Update scanner rates
    updateScannerRates(scannerData);
    await saveTrackingData(projectPath, data);
    logger.info("fp_tracker.marked_fp", {
        scanner,
        ruleId,
        reason,
        newFPRate: rule.fpRate,
    });
    return rule;
}
/**
 * Calculate adjustment factor based on rule metrics
 */
function calculateAdjustmentFactor(rule) {
    const totalReviewed = rule.confirmedTP + rule.confirmedFP;
    // Need minimum sample size for meaningful adjustment
    if (totalReviewed < 5) {
        return 1.0;
    }
    // High FP rate = lower confidence in findings
    // FP rate 0% = factor 1.0 (no adjustment)
    // FP rate 50% = factor 0.5
    // FP rate 100% = factor 0.1 (heavily penalize)
    return Math.max(0.1, 1 - (rule.fpRate * 0.9));
}
/**
 * Update scanner-level rates
 */
function updateScannerRates(scanner) {
    let totalTP = 0;
    let totalFP = 0;
    for (const rule of scanner.ruleMetrics.values()) {
        totalTP += rule.confirmedTP;
        totalFP += rule.confirmedFP;
    }
    const totalReviewed = totalTP + totalFP;
    scanner.overallTPRate = totalReviewed > 0 ? totalTP / totalReviewed : 1;
    scanner.overallFPRate = totalReviewed > 0 ? totalFP / totalReviewed : 0;
}
/**
 * Update global accuracy
 */
function updateGlobalAccuracy(data) {
    const totalReviewed = data.globalStats.totalTP + data.globalStats.totalFP;
    data.globalStats.overallAccuracy = totalReviewed > 0
        ? data.globalStats.totalTP / totalReviewed
        : 1;
}
/**
 * Adjust finding confidence based on historical FP rates
 */
export function adjustFindingConfidence(finding, metrics) {
    const baseConfidence = finding.confidence;
    if (!metrics) {
        return baseConfidence;
    }
    // Apply adjustment factor
    const adjustedConfidence = Math.round(baseConfidence * metrics.adjustmentFactor);
    // Clamp to valid range
    return Math.max(1, Math.min(100, adjustedConfidence));
}
/**
 * Get metrics for a specific rule
 */
export async function getRuleMetrics(projectPath, scanner, ruleId) {
    const data = await loadTrackingData(projectPath);
    return data.scanners[scanner]?.ruleMetrics.get(ruleId);
}
/**
 * Get all scanner metrics
 */
export async function getAllMetrics(projectPath) {
    return loadTrackingData(projectPath);
}
/**
 * Get rules with high FP rates
 */
export async function getHighFPRules(projectPath, threshold = 0.3, minSampleSize = 10) {
    const data = await loadTrackingData(projectPath);
    const highFPRules = [];
    for (const scanner of Object.values(data.scanners)) {
        for (const rule of scanner.ruleMetrics.values()) {
            const totalReviewed = rule.confirmedTP + rule.confirmedFP;
            if (totalReviewed >= minSampleSize && rule.fpRate >= threshold) {
                highFPRules.push(rule);
            }
        }
    }
    // Sort by FP rate descending
    return highFPRules.sort((a, b) => b.fpRate - a.fpRate);
}
/**
 * Generate FP tracking report
 */
export async function generateFPReport(projectPath, options) {
    const minSampleSize = options?.minSampleSize ?? 10;
    const data = await loadTrackingData(projectPath);
    // Get high FP rules
    const highFPRules = await getHighFPRules(projectPath, 0.3, minSampleSize);
    // Build scanner breakdown
    const scannerBreakdown = Object.values(data.scanners).map((scanner) => ({
        scanner: scanner.scanner,
        totalFindings: scanner.totalFindings,
        fpRate: scanner.overallFPRate,
        tpRate: scanner.overallTPRate,
        ruleCount: scanner.totalRules,
    }));
    // Generate recommendations
    const recommendations = [];
    if (highFPRules.length > 0) {
        recommendations.push(`Consider suppressing or adjusting ${highFPRules.length} rules with high FP rates.`);
    }
    for (const rule of highFPRules.slice(0, 5)) {
        if (rule.fpRate > 0.7) {
            recommendations.push(`Rule ${rule.ruleId} (${rule.scanner}) has ${Math.round(rule.fpRate * 100)}% FP rate - consider disabling.`);
        }
        else if (rule.fpRate > 0.5) {
            recommendations.push(`Rule ${rule.ruleId} (${rule.scanner}) has ${Math.round(rule.fpRate * 100)}% FP rate - review configuration.`);
        }
    }
    if (data.globalStats.overallAccuracy < 0.7) {
        recommendations.push("Overall accuracy is below 70%. Consider tuning scanner configurations.");
    }
    return {
        summary: {
            totalScanners: Object.keys(data.scanners).length,
            totalRules: Object.values(data.scanners).reduce((sum, s) => sum + s.totalRules, 0),
            totalFindings: data.globalStats.totalFindings,
            overallAccuracy: data.globalStats.overallAccuracy,
            highFPRuleCount: highFPRules.length,
        },
        scannerBreakdown,
        highFPRules,
        recommendations,
    };
}
//# sourceMappingURL=fp-tracker.js.map