/**
 * Data Flow (Taint Tracking) Engine
 *
 * Tracks how untrusted data flows from sources to sinks.
 * Core of security vulnerability detection.
 *
 * @module scanners/detection/engines/data-flow
 */
import { Project, Node } from "ts-morph";
import { readFile } from "fs/promises";
import { glob } from "glob";
const project = new Project({
    useInMemoryFileSystem: false,
    skipFileDependencyResolution: true,
});
function matchesPattern(text, pattern) {
    const normalizedPattern = pattern
        .replace(/\$[a-zA-Z_][a-zA-Z0-9_]*/g, ".*")
        .replace(/\{[^}]+\}/g, "[^.]+")
        .replace(/\./g, "\\.")
        .replace(/\(/g, "\\(")
        .replace(/\)/g, "\\)")
        .replace(/\*/g, ".*");
    try {
        const regex = new RegExp(normalizedPattern);
        return regex.test(text);
    }
    catch {
        return text.includes(pattern.replace(/[${}]/g, ""));
    }
}
function findSources(sourceFile, sources) {
    const found = [];
    sourceFile.forEachDescendant((node) => {
        const text = node.getText();
        for (const source of sources) {
            if (matchesPattern(text, source.pattern)) {
                found.push({
                    type: "source",
                    expression: text,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    pattern: source.pattern,
                });
                if (Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node)) {
                    const parent = node.getParent();
                    if (parent && Node.isVariableDeclaration(parent)) {
                        found[found.length - 1].variable = parent.getName();
                    }
                }
            }
        }
    });
    return found;
}
function findSinks(sourceFile, sinks) {
    const found = [];
    sourceFile.forEachDescendant((node) => {
        if (!Node.isCallExpression(node))
            return;
        const text = node.getText();
        const expression = node.getExpression().getText();
        for (const sink of sinks) {
            if (matchesPattern(expression, sink.pattern) || matchesPattern(text, sink.pattern)) {
                found.push({
                    type: "sink",
                    expression: text,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    pattern: sink.pattern,
                });
            }
        }
    });
    return found;
}
function findSanitizers(sourceFile, sanitizers) {
    const found = [];
    sourceFile.forEachDescendant((node) => {
        if (!Node.isCallExpression(node))
            return;
        const text = node.getText();
        for (const sanitizer of sanitizers) {
            if (matchesPattern(text, sanitizer.pattern)) {
                found.push({
                    type: "sanitizer",
                    expression: text,
                    file: sourceFile.getFilePath(),
                    line: node.getStartLineNumber(),
                    column: node.getStartLinePos() - node.getStartLinePos(true) + 1,
                    pattern: sanitizer.pattern,
                });
            }
        }
    });
    return found;
}
function extractVariablesFromExpression(expr) {
    const identifiers = [];
    const identifierRegex = /\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
    let match;
    while ((match = identifierRegex.exec(expr)) !== null) {
        const id = match[1];
        if (!["const", "let", "var", "function", "async", "await", "return", "if", "else", "for", "while", "true", "false", "null", "undefined"].includes(id)) {
            identifiers.push(id);
        }
    }
    return identifiers;
}
function traceTaintFlow(sourceFile, sources, sinks, sanitizers) {
    const paths = [];
    const taintedVars = new Set();
    for (const source of sources) {
        if (source.variable) {
            taintedVars.add(source.variable);
        }
        const sourceVars = extractVariablesFromExpression(source.expression);
        for (const v of sourceVars) {
            if (v.includes("req") || v.includes("params") || v.includes("query") || v.includes("body") || v.includes("input")) {
                taintedVars.add(v);
            }
        }
    }
    sourceFile.forEachDescendant((node) => {
        if (Node.isVariableDeclaration(node)) {
            const init = node.getInitializer();
            if (init) {
                const initText = init.getText();
                const initVars = extractVariablesFromExpression(initText);
                for (const v of initVars) {
                    if (taintedVars.has(v)) {
                        taintedVars.add(node.getName());
                        break;
                    }
                }
            }
        }
        if (Node.isBinaryExpression(node) && node.getOperatorToken().getText() === "=") {
            const left = node.getLeft();
            const right = node.getRight();
            if (Node.isIdentifier(left)) {
                const rightVars = extractVariablesFromExpression(right.getText());
                for (const v of rightVars) {
                    if (taintedVars.has(v)) {
                        taintedVars.add(left.getText());
                        break;
                    }
                }
            }
        }
    });
    const sanitizerLines = new Set(sanitizers.map((s) => s.line));
    for (const sink of sinks) {
        const sinkVars = extractVariablesFromExpression(sink.expression);
        let isTainted = false;
        for (const v of sinkVars) {
            if (taintedVars.has(v)) {
                isTainted = true;
                break;
            }
        }
        if (!isTainted)
            continue;
        let sanitized = false;
        let sanitizerPattern;
        for (const sanitizer of sanitizers) {
            if (sanitizer.line < sink.line) {
                const sanitizerVars = extractVariablesFromExpression(sanitizer.expression);
                for (const v of sanitizerVars) {
                    if (sinkVars.includes(v)) {
                        sanitized = true;
                        sanitizerPattern = sanitizer.pattern;
                        break;
                    }
                }
            }
        }
        for (const source of sources) {
            const sourceVars = source.variable
                ? [source.variable]
                : extractVariablesFromExpression(source.expression);
            let connected = false;
            for (const sv of sourceVars) {
                if (taintedVars.has(sv)) {
                    for (const sinkVar of sinkVars) {
                        if (taintedVars.has(sinkVar)) {
                            connected = true;
                            break;
                        }
                    }
                }
            }
            if (!connected)
                continue;
            paths.push({
                source: {
                    pattern: source.pattern || source.expression,
                    file: source.file,
                    line: source.line,
                    column: source.column,
                    expression: source.expression,
                },
                sink: {
                    pattern: sink.pattern || sink.expression,
                    file: sink.file,
                    line: sink.line,
                    column: sink.column,
                    expression: sink.expression,
                },
                intermediateNodes: [],
                sanitized,
                sanitizer: sanitizerPattern,
            });
        }
    }
    return paths;
}
export async function analyzeDataFlow(filePath, config) {
    try {
        const content = await readFile(filePath, "utf-8");
        const sourceFile = project.createSourceFile(`dataflow_${Date.now()}_${Math.random().toString(36).slice(2)}.ts`, content, { overwrite: true });
        const sources = findSources(sourceFile, config.sources);
        const sinks = findSinks(sourceFile, config.sinks);
        const sanitizers = config.sanitizers ? findSanitizers(sourceFile, config.sanitizers) : [];
        const paths = traceTaintFlow(sourceFile, sources, sinks, sanitizers);
        sourceFile.delete();
        return paths.filter((p) => !p.sanitized);
    }
    catch {
        return [];
    }
}
export async function runDataFlowEngine(projectPath, rules, files) {
    const matches = [];
    const targetFiles = files || (await glob("**/*.{ts,tsx,js,jsx}", {
        cwd: projectPath,
        ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**"],
        absolute: true,
    }));
    for (const rule of rules) {
        if (!rule.engines.dataFlow)
            continue;
        const config = rule.engines.dataFlow;
        for (const file of targetFiles) {
            const paths = await analyzeDataFlow(file, config);
            for (const path of paths) {
                matches.push({
                    ruleId: rule.id,
                    file: path.sink.file,
                    line: path.sink.line,
                    column: path.sink.column,
                    message: `${rule.description} - Tainted data flows from ${path.source.expression} to ${path.sink.expression}`,
                    severity: rule.severity,
                    confidence: rule.confidence,
                    category: rule.category,
                    evidence: path.sink.expression,
                    taintPath: path,
                    cweIds: rule.cweIds,
                    owaspRefs: rule.owaspRefs,
                    autofixPatternId: rule.autofixPatternId,
                });
            }
        }
    }
    return matches;
}
//# sourceMappingURL=data-flow.js.map