/**
 * Race Condition Scanner
 *
 * Detects concurrency bugs that could lead to security vulnerabilities.
 *
 * Detection targets:
 * - Go: go vet static race detection, shared state patterns
 * - Node.js: Shared mutable state + async, TOCTOU patterns
 * - Python: threading issues, GIL bypass concerns
 * - Java: Non-atomic operations, check-then-act bugs
 *
 * CWE Mappings:
 * - CWE-362: Concurrent Execution with Improper Synchronization
 * - CWE-366: Race Condition within a Thread
 * - CWE-367: TOCTOU Race Condition
 * - CWE-820: Missing Synchronization
 */
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as path from "path";
import { glob } from "glob";
const execAsync = promisify(exec);
// Node.js/JavaScript race condition patterns
const NODE_RACE_PATTERNS = [
    {
        pattern: /fs\.exists(?:Sync)?\s*\([^)]+\)[^;]*[\s\S]{0,100}fs\.(write|unlink|rename|mkdir|rmdir)/g,
        severity: "high",
        cwe: "CWE-367",
        ruleId: "race:toctou-fs",
        message: "TOCTOU race condition: fs.exists() check followed by file operation. Use fs.access() with O_CREAT or handle EEXIST/ENOENT.",
    },
    {
        pattern: /if\s*\(\s*!?\s*(?:await\s+)?fs\.(?:exists|access|stat)[\s\S]{0,200}fs\.(write|unlink|read)/g,
        severity: "high",
        cwe: "CWE-367",
        ruleId: "race:toctou-fs-async",
        message: "TOCTOU race condition: file existence check followed by operation. Another process may modify the file.",
    },
    {
        pattern: /let\s+\w+\s*=\s*(?:global|module\.exports|this)\.\w+[\s\S]{0,100}async\s+function/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:shared-state-async",
        message: "Shared mutable state accessed in async context. Consider using atomic operations or locks.",
    },
    {
        pattern: /(?:global|globalThis)\.\w+\s*=[\s\S]{0,50}(?:setInterval|setTimeout)/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:global-mutation-timer",
        message: "Global state mutated in timer callback. This may race with other code paths.",
    },
    {
        pattern: /\.then\s*\([^)]*\)\s*[\s\S]{0,50}(?:global|this)\.\w+\s*(?:\+|\-)?=/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:promise-mutation",
        message: "Shared state modified in Promise.then(). Multiple promises may race.",
    },
    {
        pattern: /if\s*\(\s*\w+\.(length|size)\s*[<>=]+\s*\d+\s*\)[\s\S]{0,100}\w+\.(push|pop|shift|delete|add)/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:check-then-mutate",
        message: "Check-then-mutate pattern on collection. Use atomic operations or synchronization.",
    },
];
// Python race condition patterns
const PYTHON_RACE_PATTERNS = [
    {
        pattern: /os\.path\.exists\s*\([^)]+\)[\s\S]{0,100}open\s*\(/g,
        severity: "high",
        cwe: "CWE-367",
        ruleId: "race:toctou-python-file",
        message: "TOCTOU race condition: os.path.exists() followed by open(). Use exception handling instead.",
    },
    {
        pattern: /threading\.(Thread|Lock|RLock|Condition)[\s\S]{0,200}(?:global|self\.)\w+\s*=/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:threading-shared-state",
        message: "Shared state modified in threaded context. Ensure proper lock acquisition.",
    },
    {
        pattern: /if\s+not\s+\w+\.locked\(\)[\s\S]{0,50}\.acquire\(/g,
        severity: "high",
        cwe: "CWE-367",
        ruleId: "race:lock-check-acquire",
        message: "Race between lock check and acquire. Use blocking acquire() or try_acquire() instead.",
    },
    {
        pattern: /global\s+\w+[\s\S]{0,100}@app\.route|def\s+\w+.*request/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:global-flask-handler",
        message: "Global variable modified in Flask route handler. This races between concurrent requests.",
    },
    {
        pattern: /multiprocessing\.Value|multiprocessing\.Array[\s\S]{0,100}(?!\bwith\s+\w+\.get_lock\(\))/g,
        severity: "medium",
        cwe: "CWE-820",
        ruleId: "race:multiprocessing-unlocked",
        message: "Multiprocessing shared value without explicit lock. Use .get_lock() for compound operations.",
    },
];
// Go race condition patterns
const GO_RACE_PATTERNS = [
    {
        pattern: /go\s+func\s*\([^)]*\)\s*\{[\s\S]{0,200}\w+\s*(?:\+|\-)?\s*=/g,
        severity: "high",
        cwe: "CWE-362",
        ruleId: "race:goroutine-shared-write",
        message: "Write to shared variable in goroutine without synchronization. Use sync.Mutex or channels.",
    },
    {
        pattern: /if\s+\w+\s*==\s*nil\s*\{[\s\S]{0,50}\w+\s*=\s*(?:make|new|&)/g,
        severity: "medium",
        cwe: "CWE-367",
        ruleId: "race:lazy-init",
        message: "Lazy initialization pattern without sync.Once. May initialize multiple times under contention.",
    },
    {
        pattern: /var\s+\w+\s+(?:map|slice|\[\])[\s\S]{0,200}go\s+func/g,
        severity: "high",
        cwe: "CWE-362",
        ruleId: "race:concurrent-map",
        message: "Map or slice shared with goroutine. Maps are not safe for concurrent use without sync.RWMutex.",
    },
    {
        pattern: /for\s+\w+,\s*\w+\s*:=\s*range[\s\S]{0,50}go\s+func\s*\(\)\s*\{/g,
        severity: "high",
        cwe: "CWE-362",
        ruleId: "race:closure-variable",
        message: "Loop variable captured by closure in goroutine. Capture by value or use Go 1.22+ semantics.",
    },
    {
        pattern: /atomic\.\w+\s*\([^)]+\)[\s\S]{0,50}(?!atomic\.)\w+\s*=/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:mixed-atomic",
        message: "Mixing atomic and non-atomic operations on same variable. Use atomic for all accesses.",
    },
];
// Java race condition patterns
const JAVA_RACE_PATTERNS = [
    {
        pattern: /if\s*\(\s*\w+\s*==\s*null\s*\)\s*\{[\s\S]{0,50}\w+\s*=\s*new\s+/g,
        severity: "medium",
        cwe: "CWE-367",
        ruleId: "race:double-checked-locking",
        message: "Potential double-checked locking issue. Use volatile + synchronized or Initialization-on-demand holder.",
    },
    {
        pattern: /(?<!synchronized\s*\([^)]+\)\s*\{[\s\S]{0,100})\w+\+\+|--\w+|\w+--/g,
        severity: "medium",
        cwe: "CWE-362",
        ruleId: "race:non-atomic-increment",
        message: "Non-atomic increment/decrement outside synchronized block. Use AtomicInteger.",
    },
    {
        pattern: /HashMap\s*<[\s\S]{0,100}(?:Thread|Runnable|@Async|CompletableFuture)/g,
        severity: "high",
        cwe: "CWE-362",
        ruleId: "race:concurrent-hashmap",
        message: "HashMap used in concurrent context. Use ConcurrentHashMap for thread safety.",
    },
    {
        pattern: /\.contains\s*\([^)]+\)\s*\)[\s\S]{0,50}\.(?:add|put|remove)\s*\(/g,
        severity: "medium",
        cwe: "CWE-367",
        ruleId: "race:check-then-modify",
        message: "Check-then-modify pattern on collection. Use putIfAbsent() or computeIfAbsent().",
    },
];
/**
 * Detect languages in the project
 */
async function detectProjectLanguages(projectPath) {
    const detection = {
        go: false,
        javascript: false,
        python: false,
        java: false,
    };
    const absolutePath = path.resolve(projectPath);
    const ignorePatterns = ["**/node_modules/**", "**/vendor/**", "**/build/**", "**/dist/**", "**/target/**"];
    // Check for Go
    const goFiles = await glob("**/*.go", { cwd: absolutePath, ignore: ignorePatterns, nodir: true });
    detection.go = goFiles.length > 0;
    // Check for JavaScript/TypeScript
    const jsFiles = await glob("**/*.{js,ts,mjs,cjs}", { cwd: absolutePath, ignore: ignorePatterns, nodir: true });
    detection.javascript = jsFiles.length > 0;
    // Check for Python
    const pyFiles = await glob("**/*.py", { cwd: absolutePath, ignore: ignorePatterns, nodir: true });
    detection.python = pyFiles.length > 0;
    // Check for Java
    const javaFiles = await glob("**/*.java", { cwd: absolutePath, ignore: ignorePatterns, nodir: true });
    detection.java = javaFiles.length > 0;
    return detection;
}
/**
 * Check if Go vet is available
 */
async function checkGoVetAvailable() {
    try {
        await execAsync("go version");
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Run go vet for race detection hints
 */
async function runGoVetRace(projectPath, timeout = 60000) {
    const findings = [];
    const absolutePath = path.resolve(projectPath);
    try {
        // Find go.mod files to identify Go modules
        const goModFiles = await glob("**/go.mod", {
            cwd: absolutePath,
            ignore: ["**/vendor/**"],
        });
        for (const goMod of goModFiles) {
            const moduleDir = path.dirname(path.join(absolutePath, goMod));
            try {
                // Run go vet with race-related checks
                const { stderr } = await execAsync("go vet -copylocks -loopclosure ./... 2>&1 || true", {
                    cwd: moduleDir,
                    timeout,
                    maxBuffer: 5 * 1024 * 1024,
                });
                // Parse go vet output
                const vetLines = stderr.split("\n").filter(line => line.trim());
                for (const line of vetLines) {
                    // Format: file.go:line:col: message
                    const match = line.match(/^([^:]+):(\d+):(\d+)?:\s*(.+)/);
                    if (match) {
                        const [, file, lineNum, col, message] = match;
                        const relativePath = path.relative(absolutePath, path.join(moduleDir, file));
                        let ruleId = "race:go-vet-generic";
                        let cwe = "CWE-362";
                        let severity = "medium";
                        if (message.includes("loop variable") || message.includes("loopclosure")) {
                            ruleId = "race:go-closure-capture";
                            severity = "high";
                        }
                        else if (message.includes("copies lock") || message.includes("copylocks")) {
                            ruleId = "race:go-lock-copy";
                            cwe = "CWE-820";
                            severity = "high";
                        }
                        findings.push({
                            scanner: "race-condition",
                            ruleId,
                            file: relativePath,
                            line: parseInt(lineNum, 10),
                            column: col ? parseInt(col, 10) : 0,
                            message: message.trim(),
                            severity,
                            confidence: 95,
                            category: "race-condition",
                            cweIds: [cwe],
                            evidence: `go vet: ${message}`,
                            metadata: { tool: "go-vet" },
                        });
                    }
                }
            }
            catch {
                // go vet failed for this module
            }
        }
    }
    catch {
        // No Go modules or go not available
    }
    return findings;
}
/**
 * Run pattern-based analysis for race conditions
 */
async function runPatternAnalysis(projectPath, patterns, filePatterns) {
    const findings = [];
    const absolutePath = path.resolve(projectPath);
    try {
        const files = await glob(filePatterns, {
            cwd: absolutePath,
            ignore: ["**/node_modules/**", "**/vendor/**", "**/build/**", "**/dist/**", "**/target/**", "**/*.test.*", "**/*.spec.*"],
            nodir: true,
        });
        for (const file of files.slice(0, 200)) { // Limit to 200 files
            try {
                const content = await fs.readFile(path.join(absolutePath, file), "utf-8");
                for (const pattern of patterns) {
                    // Reset regex lastIndex
                    pattern.pattern.lastIndex = 0;
                    let match;
                    while ((match = pattern.pattern.exec(content)) !== null) {
                        // Find line number
                        const upToMatch = content.slice(0, match.index);
                        const lineNumber = upToMatch.split("\n").length;
                        const columnNumber = match.index - upToMatch.lastIndexOf("\n");
                        findings.push({
                            scanner: "race-condition",
                            ruleId: pattern.ruleId,
                            file,
                            line: lineNumber,
                            column: columnNumber,
                            message: pattern.message,
                            severity: pattern.severity,
                            confidence: 75, // Pattern matching has lower confidence
                            category: "race-condition",
                            cweIds: [pattern.cwe],
                            evidence: `Found pattern: ${match[0].slice(0, 100).replace(/\n/g, " ")}...`,
                            metadata: {
                                context: pattern.context,
                            },
                        });
                    }
                }
            }
            catch {
                // Could not read file
            }
        }
    }
    catch {
        // Glob failed
    }
    return findings;
}
/**
 * Detect Node.js-specific race conditions in async code
 */
async function detectNodeRacePatterns(projectPath) {
    return runPatternAnalysis(projectPath, NODE_RACE_PATTERNS, ["**/*.js", "**/*.ts", "**/*.mjs", "**/*.cjs"]);
}
/**
 * Detect Python race conditions
 */
async function detectPythonRacePatterns(projectPath) {
    return runPatternAnalysis(projectPath, PYTHON_RACE_PATTERNS, ["**/*.py"]);
}
/**
 * Detect Go race conditions (pattern-based, supplements go vet)
 */
async function detectGoRacePatterns(projectPath) {
    return runPatternAnalysis(projectPath, GO_RACE_PATTERNS, ["**/*.go"]);
}
/**
 * Detect Java race conditions
 */
async function detectJavaRacePatterns(projectPath) {
    return runPatternAnalysis(projectPath, JAVA_RACE_PATTERNS, ["**/*.java"]);
}
/**
 * Main entry point: Run race condition analysis on a project
 */
export async function runRaceConditionAnalysis(projectPath, options) {
    const startTime = Date.now();
    const findings = [];
    const timeout = options?.timeout || 60000;
    try {
        // 1. Detect project languages
        const languages = await detectProjectLanguages(projectPath);
        const hasAnyLanguage = languages.go || languages.javascript || languages.python || languages.java;
        if (!hasAnyLanguage) {
            return {
                scanner: "race-condition",
                findings: [],
                duration: Date.now() - startTime,
                success: true,
                metadata: {
                    message: "No supported languages detected",
                    languages,
                },
            };
        }
        // 2. Check tool availability
        const hasGoVet = languages.go && await checkGoVetAvailable();
        // 3. Run Go analysis
        if (languages.go) {
            // Run go vet if available
            if (hasGoVet) {
                const vetFindings = await runGoVetRace(projectPath, timeout);
                findings.push(...vetFindings);
            }
            // Run pattern analysis
            const goPatternFindings = await detectGoRacePatterns(projectPath);
            findings.push(...goPatternFindings);
        }
        // 4. Run Node.js analysis
        if (languages.javascript) {
            const nodeFindings = await detectNodeRacePatterns(projectPath);
            findings.push(...nodeFindings);
        }
        // 5. Run Python analysis
        if (languages.python) {
            const pyFindings = await detectPythonRacePatterns(projectPath);
            findings.push(...pyFindings);
        }
        // 6. Run Java analysis
        if (languages.java) {
            const javaFindings = await detectJavaRacePatterns(projectPath);
            findings.push(...javaFindings);
        }
        // Deduplicate findings by file+line+ruleId
        const seen = new Set();
        const dedupedFindings = findings.filter(f => {
            const key = `${f.file}:${f.line}:${f.ruleId}`;
            if (seen.has(key))
                return false;
            seen.add(key);
            return true;
        });
        return {
            scanner: "race-condition",
            findings: dedupedFindings,
            duration: Date.now() - startTime,
            success: true,
            metadata: {
                languages,
                toolsUsed: {
                    goVet: hasGoVet,
                    patternAnalysis: true,
                },
            },
        };
    }
    catch (error) {
        return {
            scanner: "race-condition",
            findings,
            duration: Date.now() - startTime,
            success: false,
            error: error instanceof Error ? error.message : "Unknown error",
        };
    }
}
//# sourceMappingURL=race-condition.js.map