/**
 * False Positive Filter
 *
 * Context-aware filtering to reduce false positives from scanners.
 * Analyzes code context, semantic patterns, and historical data.
 *
 * @module scanners/fp-filter
 */
import { readFile } from "fs/promises";
import { join, basename } from "path";
import { logger } from "../logger.js";
/**
 * Patterns that indicate test files
 */
const TEST_FILE_PATTERNS = [
    /\.test\.[jt]sx?$/,
    /\.spec\.[jt]sx?$/,
    /__tests__\//,
    /__mocks__\//,
    /(?:^|\/)test\//,
    /(?:^|\/)tests\//,
    /\.stories\.[jt]sx?$/,
    /\.mock\.[jt]sx?$/,
    /(?:^|\/)cypress\//,
    /(?:^|\/)e2e\//,
    /(?:^|\/)playwright\//,
];
/**
 * Patterns that indicate generated code
 */
const GENERATED_CODE_PATTERNS = [
    /\.generated\.[jt]sx?$/,
    /\.gen\.[jt]sx?$/,
    /generated\//,
    /__generated__\//,
    /\.d\.ts$/,
    /swagger-client/,
    /openapi-client/,
    /graphql\.ts$/,
    /prisma\/client/,
];
/**
 * Patterns that indicate vendored/third-party code
 */
const VENDOR_PATTERNS = [
    /node_modules\//,
    /vendor\//,
    /third[_-]?party\//,
    /external\//,
    /lib\/vendor/,
    /\.min\.js$/,
    /bundle\.js$/,
];
/**
 * Patterns that indicate fixture/example files
 */
const FIXTURE_PATTERNS = [
    /fixtures?\//,
    /samples?\//,
    /examples?\//,
    /demo\//,
    /seed\//,
    /seeds\//,
    /mock[_-]?data/,
];
/**
 * Rules that are commonly false positives in certain contexts
 */
const CONTEXT_SENSITIVE_RULES = {
    "hardcoded-secret": {
        filterInTests: true,
        filterInGenerated: false,
        filterIfConstant: false,
        filterIfEnvVar: true,
    },
    "sql-injection": {
        filterInTests: true,
        filterInGenerated: true,
        filterIfConstant: true,
        filterIfEnvVar: false,
    },
    "xss": {
        filterInTests: true,
        filterInGenerated: true,
        filterIfConstant: true,
        filterIfEnvVar: false,
    },
    "command-injection": {
        filterInTests: true,
        filterInGenerated: true,
        filterIfConstant: true,
        filterIfEnvVar: false,
    },
    "path-traversal": {
        filterInTests: true,
        filterInGenerated: true,
        filterIfConstant: true,
        filterIfEnvVar: false,
    },
};
/**
 * Analyze file path to determine code context
 */
export function analyzeFilePath(filePath) {
    const normalizedPath = filePath.replace(/\\/g, "/");
    const fileName = basename(filePath);
    return {
        isTestFile: TEST_FILE_PATTERNS.some((p) => p.test(normalizedPath)),
        isGeneratedCode: GENERATED_CODE_PATTERNS.some((p) => p.test(normalizedPath)),
        isThirdPartyVendored: VENDOR_PATTERNS.some((p) => p.test(normalizedPath)),
        isMockFile: /\.mock\.[jt]sx?$/.test(fileName) || /mocks?\//i.test(normalizedPath),
        isFixtureFile: FIXTURE_PATTERNS.some((p) => p.test(normalizedPath)),
        isExampleFile: /examples?\//i.test(normalizedPath) || /demo\//i.test(normalizedPath),
    };
}
/**
 * Analyze code content for semantic context
 */
export async function analyzeCodeContext(projectPath, finding) {
    const defaultContext = {
        hasValidation: false,
        hasEncoding: false,
        isSanitized: false,
        isConstant: false,
        isEnvironmentVariable: false,
    };
    if (!finding.file) {
        return defaultContext;
    }
    try {
        const filePath = join(projectPath, finding.file);
        const content = await readFile(filePath, "utf-8");
        const lines = content.split("\n");
        // Get surrounding context (5 lines before and after)
        const startLine = Math.max(0, (finding.line || 1) - 6);
        const endLine = Math.min(lines.length, (finding.line || 1) + 5);
        const context = lines.slice(startLine, endLine).join("\n");
        // Check for validation patterns
        const validationPatterns = [
            /validate/i,
            /sanitize/i,
            /escape/i,
            /encode/i,
            /\.parse\(/,
            /z\.\w+\(/, // Zod validation
            /yup\.\w+/, // Yup validation
            /joi\.\w+/, // Joi validation
            /validator\./,
        ];
        const hasValidation = validationPatterns.some((p) => p.test(context));
        // Check for encoding patterns
        const encodingPatterns = [
            /encodeURIComponent/,
            /encodeURI/,
            /htmlEncode/,
            /escapeHtml/,
            /DOMPurify/,
            /sanitizeHtml/,
        ];
        const hasEncoding = encodingPatterns.some((p) => p.test(context));
        // Check for sanitization
        const sanitizationPatterns = [
            /sanitize/i,
            /clean/i,
            /purify/i,
            /strip/i,
            /filter\s*\(/,
        ];
        const isSanitized = sanitizationPatterns.some((p) => p.test(context));
        // Check if value is constant
        const line = lines[(finding.line || 1) - 1] || "";
        const isConstant = /const\s+\w+\s*=\s*["'`]/.test(line) ||
            /readonly\s/.test(line) ||
            /as\s+const/.test(line);
        // Check if value comes from environment variable
        const isEnvironmentVariable = /process\.env\./i.test(context) ||
            /import\.meta\.env/i.test(context) ||
            /\$\{.*ENV.*\}/i.test(context);
        return {
            hasValidation,
            hasEncoding,
            isSanitized,
            isConstant,
            isEnvironmentVariable,
        };
    }
    catch (error) {
        logger.debug("fp_filter.analyze_failed", {
            file: finding.file,
            error: String(error),
        });
        return defaultContext;
    }
}
/**
 * Determine if a finding should be filtered as a false positive
 */
export function shouldFilter(finding, context) {
    const { codeContext, semanticContext, historicalContext } = context;
    // Always filter vendored/third-party code
    if (codeContext.isThirdPartyVendored) {
        return {
            filter: true,
            reason: "Third-party/vendored code",
            confidence: 95,
            suggestion: "suppress",
        };
    }
    // Get rule-specific filtering rules
    const category = finding.category || extractCategory(finding.ruleId);
    const ruleConfig = CONTEXT_SENSITIVE_RULES[category];
    if (ruleConfig) {
        // Filter in test files for certain rules
        if (ruleConfig.filterInTests && codeContext.isTestFile) {
            return {
                filter: true,
                reason: "Test file - rule typically FP in tests",
                confidence: 85,
                suggestion: "suppress",
            };
        }
        // Filter in generated code
        if (ruleConfig.filterInGenerated && codeContext.isGeneratedCode) {
            return {
                filter: true,
                reason: "Generated code",
                confidence: 90,
                suggestion: "suppress",
            };
        }
        // Filter if value is constant
        if (ruleConfig.filterIfConstant && semanticContext.isConstant) {
            return {
                filter: true,
                reason: "Constant value - not user-controlled",
                confidence: 75,
                suggestion: "review",
            };
        }
        // Filter if environment variable
        if (ruleConfig.filterIfEnvVar && semanticContext.isEnvironmentVariable) {
            return {
                filter: true,
                reason: "Environment variable reference",
                confidence: 80,
                suggestion: "review",
            };
        }
    }
    // Filter fixture/example files with lower confidence
    if (codeContext.isFixtureFile || codeContext.isExampleFile) {
        return {
            filter: true,
            reason: "Fixture/example file",
            confidence: 70,
            suggestion: "review",
        };
    }
    // Check if sanitization is present
    if (semanticContext.isSanitized || semanticContext.hasValidation || semanticContext.hasEncoding) {
        return {
            filter: false,
            reason: "Sanitization detected but manual review recommended",
            confidence: 60,
            suggestion: "review",
        };
    }
    // Check historical suppressions
    if (historicalContext.suppressions.includes(finding.ruleId)) {
        return {
            filter: true,
            reason: "Previously suppressed rule",
            confidence: 70,
            suggestion: "review",
        };
    }
    // Check historical accuracy
    if (historicalContext.ruleAccuracy < 0.3) {
        return {
            filter: false,
            reason: `Low historical accuracy (${Math.round(historicalContext.ruleAccuracy * 100)}%)`,
            confidence: 50,
            suggestion: "review",
        };
    }
    // No filtering
    return {
        filter: false,
        confidence: 100,
        suggestion: "confirm",
    };
}
/**
 * Extract category from rule ID
 */
function extractCategory(ruleId) {
    // Common patterns: "scanner:category.rule" or "category-rule"
    const colonMatch = ruleId.match(/^[^:]+:([^.]+)/);
    if (colonMatch) {
        return colonMatch[1];
    }
    const dashMatch = ruleId.match(/^([a-z-]+)-\d+$/i);
    if (dashMatch) {
        return dashMatch[1];
    }
    return ruleId;
}
/**
 * Filter findings and return filtered results with reasons
 */
export async function filterFindings(projectPath, findings, options) {
    const minConfidence = options?.minConfidence ?? 70;
    const historicalData = options?.historicalData ?? new Map();
    const filtered = [];
    const removed = [];
    const byReason = {};
    for (const finding of findings) {
        // Build context
        const codeContext = analyzeFilePath(finding.file);
        const semanticContext = await analyzeCodeContext(projectPath, finding);
        const ruleHistory = historicalData.get(finding.ruleId);
        const historicalContext = {
            previousFPs: [],
            ruleAccuracy: ruleHistory?.fpRate ?? 1.0,
            suppressions: ruleHistory?.suppressions ?? [],
        };
        const context = {
            codeContext,
            semanticContext,
            historicalContext,
        };
        const result = shouldFilter(finding, context);
        if (result.filter && result.confidence >= minConfidence) {
            removed.push({
                finding,
                reason: result.reason || "Filtered",
                confidence: result.confidence,
            });
            byReason[result.reason || "Unknown"] = (byReason[result.reason || "Unknown"] || 0) + 1;
        }
        else {
            filtered.push(finding);
        }
    }
    logger.info("fp_filter.complete", {
        total: findings.length,
        kept: filtered.length,
        filtered: removed.length,
    });
    return {
        filtered,
        removed,
        stats: {
            total: findings.length,
            kept: filtered.length,
            filtered: removed.length,
            byReason,
        },
    };
}
/**
 * Get suggested suppressions for a finding
 */
export function getSuppressSuggestion(finding, context) {
    const scanner = finding.scanner;
    const ruleId = finding.ruleId;
    // Generate scanner-specific suppression comments
    let inlineComment;
    let fileComment;
    switch (scanner) {
        case "semgrep":
            inlineComment = `// nosemgrep: ${ruleId}`;
            fileComment = `// nosemgrep`;
            break;
        case "eslint":
            inlineComment = `// eslint-disable-next-line ${ruleId}`;
            fileComment = `/* eslint-disable ${ruleId} */`;
            break;
        case "gitleaks":
            inlineComment = `// gitleaks:allow`;
            fileComment = `# gitleaks:allow`;
            break;
        case "bandit":
            inlineComment = `# nosec ${ruleId}`;
            fileComment = `# nosec`;
            break;
        default:
            inlineComment = `// @suppress ${ruleId}`;
            fileComment = `// @suppress-file ${ruleId}`;
    }
    return {
        suppressionComment: `Suppressed: ${finding.message} (${context.codeContext.isTestFile ? "test file" : "reviewed"})`,
        inlineSuppress: inlineComment,
        fileSuppress: fileComment,
    };
}
//# sourceMappingURL=fp-filter.js.map