/**
 * Data Flow Analysis Module
 *
 * Tracks data flow from sources (user input, API responses) to sinks
 * (database writes, command execution) to identify potential vulnerabilities.
 *
 * This module provides the foundation for semantic vulnerability detection
 * by identifying risky data flows that bypass sanitization.
 *
 * @module analysis/data-flow
 */
import { readFile } from "fs/promises";
import { join, extname } from "path";
import { logger } from "../logger.js";
// ============================================================================
// Pattern Definitions
// ============================================================================
/**
 * Patterns for identifying data sources by language
 */
const SOURCE_PATTERNS = {
    javascript: [
        // Express.js patterns
        { pattern: /req\.body(?:\.\w+)?/g, type: "user-input", trustLevel: "untrusted" },
        { pattern: /req\.params(?:\.\w+)?/g, type: "url-param", trustLevel: "untrusted" },
        { pattern: /req\.query(?:\.\w+)?/g, type: "url-param", trustLevel: "untrusted" },
        { pattern: /req\.headers(?:\[['"][^'"]+['"]\])?/g, type: "header", trustLevel: "untrusted" },
        { pattern: /req\.cookies(?:\.\w+)?/g, type: "cookie", trustLevel: "untrusted" },
        // Fetch/API responses
        { pattern: /\.json\(\)/g, type: "api-response", trustLevel: "semi-trusted" },
        { pattern: /await\s+fetch\([^)]+\)/g, type: "api-response", trustLevel: "semi-trusted" },
        // Environment variables
        { pattern: /process\.env(?:\.\w+|\[['"][^'"]+['"]\])/g, type: "env-var", trustLevel: "trusted" },
        // File reads
        { pattern: /readFile(?:Sync)?\([^)]+\)/g, type: "file-read", trustLevel: "semi-trusted" },
        { pattern: /fs\.read(?:File)?(?:Sync)?\([^)]+\)/g, type: "file-read", trustLevel: "semi-trusted" },
    ],
    typescript: [], // Same as JavaScript, will extend
    python: [
        // Flask patterns
        { pattern: /request\.(?:form|json|args|data|values)(?:\[['"][^'"]+['"]\])?/g, type: "user-input", trustLevel: "untrusted" },
        { pattern: /request\.headers(?:\[['"][^'"]+['"]\])?/g, type: "header", trustLevel: "untrusted" },
        { pattern: /request\.cookies(?:\[['"][^'"]+['"]\])?/g, type: "cookie", trustLevel: "untrusted" },
        // Django patterns
        { pattern: /request\.(?:GET|POST|FILES)(?:\[['"][^'"]+['"]\])?/g, type: "user-input", trustLevel: "untrusted" },
        // Environment
        { pattern: /os\.(?:getenv|environ)(?:\[['"][^'"]+['"]\]|\(['"][^'"]+['"]\))/g, type: "env-var", trustLevel: "trusted" },
        // File reads
        { pattern: /open\([^)]+\)\.read\(\)/g, type: "file-read", trustLevel: "semi-trusted" },
    ],
};
/**
 * Patterns for identifying data sinks by language
 */
const SINK_PATTERNS = {
    javascript: [
        // SQL
        { pattern: /\.query\([^)]+\)/g, type: "sql-query", riskLevel: "critical", function: "query" },
        { pattern: /\.execute\([^)]+\)/g, type: "sql-query", riskLevel: "critical", function: "execute" },
        { pattern: /\.raw\([^)]+\)/g, type: "sql-query", riskLevel: "critical", function: "raw" },
        // Command execution
        { pattern: /exec\([^)]+\)/g, type: "command-exec", riskLevel: "critical", function: "exec" },
        { pattern: /execSync\([^)]+\)/g, type: "command-exec", riskLevel: "critical", function: "execSync" },
        { pattern: /spawn\([^)]+\)/g, type: "command-exec", riskLevel: "high", function: "spawn" },
        // Eval
        { pattern: /eval\([^)]+\)/g, type: "eval", riskLevel: "critical", function: "eval" },
        { pattern: /Function\([^)]+\)/g, type: "eval", riskLevel: "critical", function: "Function" },
        { pattern: /vm\.runInContext\([^)]+\)/g, type: "eval", riskLevel: "critical", function: "vm.runInContext" },
        // File writes
        { pattern: /writeFile(?:Sync)?\([^)]+\)/g, type: "file-write", riskLevel: "high", function: "writeFile" },
        { pattern: /fs\.write(?:File)?(?:Sync)?\([^)]+\)/g, type: "file-write", riskLevel: "high", function: "fs.writeFile" },
        // HTML rendering
        { pattern: /\.innerHTML\s*=/g, type: "html-render", riskLevel: "high", function: "innerHTML" },
        { pattern: /document\.write\([^)]+\)/g, type: "html-render", riskLevel: "high", function: "document.write" },
        // Redirects
        { pattern: /res\.redirect\([^)]+\)/g, type: "redirect", riskLevel: "medium", function: "res.redirect" },
        { pattern: /location\.href\s*=/g, type: "redirect", riskLevel: "medium", function: "location.href" },
    ],
    typescript: [], // Same as JavaScript
    python: [
        // SQL
        { pattern: /\.execute\([^)]+\)/g, type: "sql-query", riskLevel: "critical", function: "execute" },
        { pattern: /cursor\.execute\([^)]+\)/g, type: "sql-query", riskLevel: "critical", function: "cursor.execute" },
        // Command execution
        { pattern: /os\.system\([^)]+\)/g, type: "command-exec", riskLevel: "critical", function: "os.system" },
        { pattern: /subprocess\.(?:run|call|Popen)\([^)]+\)/g, type: "command-exec", riskLevel: "critical", function: "subprocess" },
        // Eval
        { pattern: /eval\([^)]+\)/g, type: "eval", riskLevel: "critical", function: "eval" },
        { pattern: /exec\([^)]+\)/g, type: "eval", riskLevel: "critical", function: "exec" },
        // File writes
        { pattern: /open\([^,]+,\s*['"]w['"]\)/g, type: "file-write", riskLevel: "high", function: "open(write)" },
    ],
};
/**
 * Patterns for identifying sanitizers
 */
const SANITIZER_PATTERNS = {
    javascript: [
        { pattern: /escape(?:Html)?\(/g, function: "escapeHtml", sanitizesFor: ["html-render"] },
        { pattern: /sanitize(?:Html)?\(/g, function: "sanitizeHtml", sanitizesFor: ["html-render"] },
        { pattern: /DOMPurify\.sanitize\(/g, function: "DOMPurify.sanitize", sanitizesFor: ["html-render"] },
        { pattern: /encodeURIComponent\(/g, function: "encodeURIComponent", sanitizesFor: ["redirect", "url-param"] },
        { pattern: /parameterized|prepared|placeholder/gi, function: "prepared statement", sanitizesFor: ["sql-query"] },
        { pattern: /\$\d+|\?/g, function: "parameterized query", sanitizesFor: ["sql-query"] },
    ],
    python: [
        { pattern: /escape\(/g, function: "escape", sanitizesFor: ["html-render"] },
        { pattern: /bleach\.clean\(/g, function: "bleach.clean", sanitizesFor: ["html-render"] },
        { pattern: /quote\(/g, function: "quote", sanitizesFor: ["sql-query"] },
        { pattern: /%s|:\w+/g, function: "parameterized", sanitizesFor: ["sql-query"] },
    ],
};
// Copy JS patterns to TS
SOURCE_PATTERNS.typescript = SOURCE_PATTERNS.javascript;
SINK_PATTERNS.typescript = SINK_PATTERNS.javascript;
SANITIZER_PATTERNS.typescript = SANITIZER_PATTERNS.javascript;
// ============================================================================
// Analysis Functions
// ============================================================================
/**
 * Get language from file extension
 */
function getLanguage(filePath) {
    const ext = extname(filePath).toLowerCase();
    const langMap = {
        ".js": "javascript",
        ".mjs": "javascript",
        ".cjs": "javascript",
        ".jsx": "javascript",
        ".ts": "typescript",
        ".tsx": "typescript",
        ".py": "python",
    };
    return langMap[ext] || "unknown";
}
/**
 * Generate a unique ID
 */
function generateId(prefix) {
    return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
}
/**
 * Find line number for a match position
 */
function getLineNumber(content, position) {
    return content.slice(0, position).split("\n").length;
}
/**
 * Get code snippet around a position
 */
function getSnippet(content, line, contextLines = 1) {
    const lines = content.split("\n");
    const start = Math.max(0, line - contextLines - 1);
    const end = Math.min(lines.length, line + contextLines);
    return lines.slice(start, end).join("\n");
}
/**
 * Identify data sources in a file
 */
async function identifySourcesInFile(filePath, content) {
    const language = getLanguage(filePath);
    const patterns = SOURCE_PATTERNS[language] || [];
    const sources = [];
    for (const { pattern, type, trustLevel } of patterns) {
        const regex = new RegExp(pattern.source, pattern.flags);
        let match;
        while ((match = regex.exec(content)) !== null) {
            const line = getLineNumber(content, match.index);
            sources.push({
                id: generateId("src"),
                type,
                location: {
                    file: filePath,
                    line,
                    snippet: getSnippet(content, line),
                },
                variable: match[0],
                trustLevel,
                description: `${type} at ${filePath}:${line}`,
                pattern: pattern.source,
            });
        }
    }
    return sources;
}
/**
 * Identify data sinks in a file
 */
async function identifySinksInFile(filePath, content) {
    const language = getLanguage(filePath);
    const patterns = SINK_PATTERNS[language] || [];
    const sinks = [];
    for (const { pattern, type, riskLevel, function: fn } of patterns) {
        const regex = new RegExp(pattern.source, pattern.flags);
        let match;
        while ((match = regex.exec(content)) !== null) {
            const line = getLineNumber(content, match.index);
            sinks.push({
                id: generateId("sink"),
                type,
                location: {
                    file: filePath,
                    line,
                    snippet: getSnippet(content, line),
                },
                function: fn,
                riskLevel,
                description: `${type} (${fn}) at ${filePath}:${line}`,
                pattern: pattern.source,
            });
        }
    }
    return sinks;
}
/**
 * Identify sanitizers in a file
 */
async function identifySanitizersInFile(filePath, content) {
    const language = getLanguage(filePath);
    const patterns = SANITIZER_PATTERNS[language] || [];
    const sanitizers = [];
    for (const { pattern, function: fn, sanitizesFor } of patterns) {
        const regex = new RegExp(pattern.source, pattern.flags);
        let match;
        while ((match = regex.exec(content)) !== null) {
            const line = getLineNumber(content, match.index);
            sanitizers.push({
                id: generateId("san"),
                function: fn,
                location: {
                    file: filePath,
                    line,
                    snippet: getSnippet(content, line),
                },
                sanitizesFor,
                description: `Sanitizer ${fn} at ${filePath}:${line}`,
            });
        }
    }
    return sanitizers;
}
/**
 * Determine if a source-sink pair has a sanitizer between them
 */
function hasSanitizerBetween(source, sink, sanitizers) {
    // Simple heuristic: check if there's a sanitizer in the same file
    // between the source and sink lines, and if it sanitizes for this sink type
    const relevantSanitizers = sanitizers.filter((s) => s.location.file === source.location.file &&
        s.location.file === sink.location.file &&
        s.location.line > source.location.line &&
        s.location.line < sink.location.line &&
        s.sanitizesFor.includes(sink.type));
    return {
        sanitized: relevantSanitizers.length > 0,
        sanitizerLocations: relevantSanitizers.map((s) => s.location),
    };
}
/**
 * Calculate risk level for a flow
 */
function calculateFlowRisk(source, sink, sanitized) {
    if (sanitized) {
        return "info";
    }
    if (source.trustLevel === "untrusted") {
        if (sink.riskLevel === "critical")
            return "critical";
        if (sink.riskLevel === "high")
            return "high";
        return "medium";
    }
    if (source.trustLevel === "semi-trusted") {
        if (sink.riskLevel === "critical")
            return "high";
        if (sink.riskLevel === "high")
            return "medium";
        return "low";
    }
    return "low";
}
/**
 * Identify potential data flows between sources and sinks
 */
function identifyFlows(sources, sinks, sanitizers) {
    const flows = [];
    // Simple heuristic: match sources and sinks in the same file
    // More sophisticated analysis would use actual data flow tracking
    for (const source of sources) {
        for (const sink of sinks) {
            // Only connect sources and sinks in the same file (simplified)
            if (source.location.file !== sink.location.file) {
                continue;
            }
            // Only connect if sink comes after source
            if (sink.location.line <= source.location.line) {
                continue;
            }
            const { sanitized, sanitizerLocations } = hasSanitizerBetween(source, sink, sanitizers);
            const riskLevel = calculateFlowRisk(source, sink, sanitized);
            flows.push({
                source,
                sink,
                path: [source.location, sink.location],
                passesThroughSanitizer: sanitized,
                sanitizerLocations: sanitized ? sanitizerLocations : undefined,
                riskLevel,
                description: sanitized
                    ? `Sanitized flow from ${source.type} to ${sink.type}`
                    : `Unsanitized ${source.trustLevel} data flows to ${sink.type}`,
            });
        }
    }
    return flows;
}
// ============================================================================
// Public API
// ============================================================================
/**
 * Build data flow context for a project
 */
export async function buildDataFlowContext(projectPath, files) {
    const allSources = [];
    const allSinks = [];
    const allSanitizers = [];
    // If no files provided, this would scan the project
    // For now, accept a list of files to analyze
    const filesToAnalyze = files || [];
    for (const filePath of filesToAnalyze) {
        try {
            const fullPath = join(projectPath, filePath);
            const content = await readFile(fullPath, "utf-8");
            const sources = await identifySourcesInFile(filePath, content);
            const sinks = await identifySinksInFile(filePath, content);
            const sanitizers = await identifySanitizersInFile(filePath, content);
            allSources.push(...sources);
            allSinks.push(...sinks);
            allSanitizers.push(...sanitizers);
        }
        catch (error) {
            logger.warn("data-flow.file_read_error", {
                file: filePath,
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }
    const flows = identifyFlows(allSources, allSinks, allSanitizers);
    const riskyFlows = flows.filter((f) => !f.passesThroughSanitizer);
    const sanitizedFlows = flows.filter((f) => f.passesThroughSanitizer);
    return {
        sources: allSources,
        sinks: allSinks,
        sanitizers: allSanitizers,
        flows,
        stats: {
            totalSources: allSources.length,
            totalSinks: allSinks.length,
            totalSanitizers: allSanitizers.length,
            totalFlows: flows.length,
            riskyFlows: riskyFlows.length,
            sanitizedFlows: sanitizedFlows.length,
        },
    };
}
/**
 * Get only risky (unsanitized) flows
 */
export function getRiskyFlows(context) {
    return context.flows.filter((f) => !f.passesThroughSanitizer);
}
/**
 * Get flows by severity
 */
export function getFlowsBySeverity(context, severity) {
    return context.flows.filter((f) => f.riskLevel === severity);
}
/**
 * Format context for LLM analysis
 */
export function formatContextForLLM(riskyFlows, maxFlows = 10) {
    const flows = riskyFlows.slice(0, maxFlows);
    let output = "# Risky Data Flows Detected\n\n";
    output += `Found ${riskyFlows.length} unsanitized data flows. Showing top ${flows.length}:\n\n`;
    for (const flow of flows) {
        output += `## ${flow.source.type} → ${flow.sink.type} (${flow.riskLevel})\n\n`;
        output += `**Source:** ${flow.source.location.file}:${flow.source.location.line}\n`;
        output += `\`\`\`\n${flow.source.location.snippet || flow.source.variable}\n\`\`\`\n\n`;
        output += `**Sink:** ${flow.sink.location.file}:${flow.sink.location.line}\n`;
        output += `\`\`\`\n${flow.sink.location.snippet || flow.sink.function}\n\`\`\`\n\n`;
        output += `**Risk:** ${flow.description}\n\n---\n\n`;
    }
    return output;
}
//# sourceMappingURL=data-flow.js.map