/**
 * Sandbox Audit Scanner
 *
 * Verifies that MCP tools don't escape their declared surface area.
 * Performs static analysis to detect potential sandbox escapes:
 * - child_process usage in "read-only" tools
 * - eval/Function usage for code execution
 * - Raw network access in local-scoped tools
 * - Filesystem access outside declared paths
 *
 * @module scanners/agent/sandbox-audit
 */
import { createHash } from "crypto";
import { readFile, readdir } from "fs/promises";
import { join, extname } from "path";
const ESCAPE_PATTERNS = [
    // Child process execution
    {
        type: "child_process",
        pattern: /require\s*\(\s*['"`]child_process['"`]\s*\)|from\s+['"`]child_process['"`]|import\s+.*['"`]child_process['"`]/g,
        severity: "critical",
        description: "Direct child_process import - can execute arbitrary commands",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    {
        type: "child_process",
        pattern: /exec\s*\(|execSync\s*\(|spawn\s*\(|spawnSync\s*\(|execFile\s*\(|fork\s*\(/g,
        severity: "critical",
        description: "Process execution function - can execute arbitrary commands",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // Eval and dynamic code execution
    {
        type: "eval",
        pattern: /\beval\s*\(/g,
        severity: "critical",
        description: "eval() usage - can execute arbitrary code",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    {
        type: "Function",
        pattern: /new\s+Function\s*\(/g,
        severity: "critical",
        description: "new Function() - dynamic code execution",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // VM module
    {
        type: "vm",
        pattern: /require\s*\(\s*['"`]vm['"`]\s*\)|from\s+['"`]vm['"`]/g,
        severity: "high",
        description: "VM module import - sandboxed but escapable",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // Dynamic require/import
    {
        type: "require",
        pattern: /require\s*\(\s*[^'"`]/g,
        severity: "high",
        description: "Dynamic require() - can load arbitrary modules",
        contexts: [".js", ".ts", ".cjs"],
    },
    {
        type: "import-dynamic",
        pattern: /import\s*\(\s*[^'"`]/g,
        severity: "high",
        description: "Dynamic import() - can load arbitrary modules",
        contexts: [".js", ".ts", ".mjs"],
    },
    // Filesystem outside declared scope
    {
        type: "fs-outside-scope",
        pattern: /require\s*\(\s*['"`]fs['"`]\s*\)|from\s+['"`]fs['"`]|from\s+['"`]fs\/promises['"`]|from\s+['"`]node:fs['"`]/g,
        severity: "high",
        description: "Direct fs module access - may access outside declared paths",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // Network access patterns
    {
        type: "net-undeclared",
        pattern: /require\s*\(\s*['"`](?:net|http|https|http2|dgram)['"`]\s*\)|from\s+['"`](?:net|http|https|http2|dgram)['"`]/g,
        severity: "critical",
        description: "Network module import - can make network requests",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    {
        type: "net-undeclared",
        pattern: /\bfetch\s*\(|axios\.|got\(|request\(|superagent\./g,
        severity: "high",
        description: "Network request library usage",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // Environment access
    {
        type: "env-access",
        pattern: /process\.env\[|process\.env\./g,
        severity: "medium",
        description: "Environment variable access - may leak secrets",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
    // Process access
    {
        type: "process-access",
        pattern: /process\.exit|process\.kill|process\.abort/g,
        severity: "medium",
        description: "Process control - can terminate or affect process",
        contexts: [".js", ".ts", ".mjs", ".cjs"],
    },
];
// ============================================================================
// File Scanning
// ============================================================================
/**
 * Scan a file for escape patterns
 */
async function scanFileForEscapes(filePath, content) {
    const findings = [];
    const ext = extname(filePath).toLowerCase();
    const lines = content.split("\n");
    for (const escapePattern of ESCAPE_PATTERNS) {
        // Skip if not relevant for this file type
        if (escapePattern.contexts && !escapePattern.contexts.includes(ext)) {
            continue;
        }
        // Reset regex state
        escapePattern.pattern.lastIndex = 0;
        // Find all matches
        let match;
        while ((match = escapePattern.pattern.exec(content)) !== null) {
            // Calculate line number
            const beforeMatch = content.substring(0, match.index);
            const lineNumber = beforeMatch.split("\n").length;
            // Get line content
            const lineContent = lines[lineNumber - 1] || "";
            // Skip if in a comment
            if (isInComment(lineContent, match[0])) {
                continue;
            }
            findings.push({
                escape: escapePattern.type,
                tool: "", // Will be filled in later
                file: filePath,
                line: lineNumber,
                evidence: lineContent.trim().slice(0, 200),
                severity: escapePattern.severity,
                description: escapePattern.description,
            });
        }
    }
    return findings;
}
/**
 * Check if match is inside a comment
 */
function isInComment(line, match) {
    const matchIndex = line.indexOf(match);
    // Check for line comment
    const lineCommentIndex = line.indexOf("//");
    if (lineCommentIndex !== -1 && lineCommentIndex < matchIndex) {
        return true;
    }
    // Check for block comment start (simplified)
    const blockCommentIndex = line.indexOf("/*");
    if (blockCommentIndex !== -1 && blockCommentIndex < matchIndex) {
        return true;
    }
    return false;
}
/**
 * Recursively scan directory for source files
 */
async function scanDirectory(dirPath, options) {
    const findings = [];
    const maxDepth = options.maxDepth ?? 10;
    const exclude = options.exclude ?? [/node_modules/, /\.git/, /dist/, /build/];
    async function scan(currentPath, depth) {
        if (depth > maxDepth)
            return;
        try {
            const entries = await readdir(currentPath, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = join(currentPath, entry.name);
                // Check exclusions
                if (exclude.some((re) => re.test(fullPath))) {
                    continue;
                }
                if (entry.isDirectory()) {
                    await scan(fullPath, depth + 1);
                }
                else if (entry.isFile()) {
                    const ext = extname(entry.name).toLowerCase();
                    if ([".js", ".ts", ".mjs", ".cjs", ".jsx", ".tsx"].includes(ext)) {
                        try {
                            const content = await readFile(fullPath, "utf-8");
                            const fileFindings = await scanFileForEscapes(fullPath, content);
                            findings.push(...fileFindings);
                        }
                        catch {
                            // Skip unreadable files
                        }
                    }
                }
            }
        }
        catch {
            // Skip unreadable directories
        }
    }
    await scan(dirPath, 0);
    return findings;
}
// ============================================================================
// Tool Mapping
// ============================================================================
/**
 * Try to map findings to tools based on file paths
 *
 * This is a heuristic - assumes tool implementations are in files
 * named after the tool or in directories with tool names.
 */
function mapFindingsToTools(findings, manifest) {
    const toolNames = manifest.tools.map((t) => t.name.toLowerCase());
    for (const finding of findings) {
        const filePath = finding.file.toLowerCase();
        // Try to match tool name in file path
        for (const toolName of toolNames) {
            // Normalize tool name for file matching
            const normalized = toolName
                .replace(/_/g, "[-_]?")
                .replace(/-/g, "[-_]?");
            const regex = new RegExp(`[/\\\\]${normalized}[./\\\\]|${normalized}\\.(ts|js)$`, "i");
            if (regex.test(filePath)) {
                finding.tool = toolName;
                break;
            }
        }
        // If still no tool, use filename
        if (!finding.tool) {
            const filename = filePath.split(/[/\\]/).pop() || "";
            finding.tool = filename.replace(/\.(ts|js|mjs|cjs)$/i, "");
        }
    }
    return findings;
}
/**
 * Filter findings based on tool declarations
 */
function filterByDeclarations(findings, manifest) {
    const filteredFindings = [];
    const toolMap = new Map(manifest.tools.map((t) => [t.name.toLowerCase(), t]));
    for (const finding of findings) {
        const tool = toolMap.get(finding.tool.toLowerCase());
        if (!tool) {
            // Can't validate - keep the finding
            filteredFindings.push(finding);
            continue;
        }
        // Check if finding contradicts tool's declarations
        switch (finding.escape) {
            case "child_process":
            case "eval":
            case "Function":
                // Always report these - they're dangerous
                filteredFindings.push(finding);
                break;
            case "net-undeclared":
                // Only report if tool doesn't declare network access
                if (!tool.networkAccess) {
                    filteredFindings.push(finding);
                }
                break;
            case "fs-outside-scope":
                // Only report for read-only tools
                if (tool.readOnlyHint) {
                    finding.description = "Filesystem access in read-only tool";
                    filteredFindings.push(finding);
                }
                break;
            case "env-access":
                // Report for non-destructive tools
                if (!tool.destructiveHint) {
                    filteredFindings.push(finding);
                }
                break;
            default:
                filteredFindings.push(finding);
        }
    }
    return filteredFindings;
}
// ============================================================================
// Finding Conversion
// ============================================================================
/**
 * Convert sandbox findings to deterministic findings
 */
function sandboxToDeterministicFindings(findings) {
    const deterministicFindings = [];
    for (const finding of findings) {
        deterministicFindings.push({
            scanner: "semgrep",
            ruleId: `sandbox-audit:${finding.escape}`,
            file: finding.file,
            line: finding.line,
            message: `${finding.escape}: ${finding.description}${finding.tool ? ` in tool "${finding.tool}"` : ""}`,
            severity: finding.severity,
            confidence: 100,
            evidence: finding.evidence,
            metadata: {
                agentScanner: "sandbox-audit",
                escapeType: finding.escape,
                tool: finding.tool,
            },
        });
    }
    // Add summary
    if (findings.length > 0) {
        const byType = {};
        const bySeverity = {
            critical: 0,
            high: 0,
            medium: 0,
            low: 0,
            info: 0,
        };
        for (const f of findings) {
            byType[f.escape] = (byType[f.escape] || 0) + 1;
            bySeverity[f.severity]++;
        }
        deterministicFindings.push({
            scanner: "semgrep",
            ruleId: "sandbox-audit:summary",
            file: "mcp-manifest",
            line: 0,
            message: `Sandbox audit found ${findings.length} potential escape(s): ${bySeverity.critical} critical, ${bySeverity.high} high`,
            severity: bySeverity.critical > 0 ? "critical" : bySeverity.high > 0 ? "high" : "medium",
            confidence: 100,
            evidence: [
                `Total findings: ${findings.length}`,
                `By type:`,
                ...Object.entries(byType).map(([type, count]) => `  - ${type}: ${count}`),
            ].join("\n"),
            metadata: {
                agentScanner: "sandbox-audit",
                totalFindings: findings.length,
                byType,
                bySeverity,
            },
        });
    }
    return deterministicFindings;
}
// ============================================================================
// Main Scanner Function
// ============================================================================
/**
 * Run sandbox audit scanner
 */
export async function runSandboxAudit(manifest, options) {
    const startTime = Date.now();
    const sourcePath = options?.sourcePath || process.cwd();
    // Scan source directory
    let findings = await scanDirectory(sourcePath, {
        maxDepth: options?.maxDepth,
        exclude: options?.exclude,
    });
    // Map findings to tools
    findings = mapFindingsToTools(findings, manifest);
    // Filter by tool declarations
    findings = filterByDeclarations(findings, manifest);
    // Filter by specific tools if requested
    if (options?.tools && options.tools.length > 0) {
        const toolSet = new Set(options.tools.map((t) => t.toLowerCase()));
        findings = findings.filter((f) => toolSet.has(f.tool.toLowerCase()));
    }
    // Convert to deterministic findings
    const deterministicFindings = sandboxToDeterministicFindings(findings);
    // Calculate manifest hash
    const manifestHash = createHash("sha256")
        .update(JSON.stringify(manifest))
        .digest("hex");
    return {
        scanner: "sandbox-audit",
        findings: deterministicFindings,
        duration: Date.now() - startTime,
        success: true,
        mcpServerName: manifest.name,
        mcpServerVersion: manifest.version,
        manifestHash,
        version: "1.0.0",
        rulesUsed: ["static-analysis"],
    };
}
/**
 * Check if sandbox audit is available
 */
export async function checkSandboxAuditAvailable() {
    return {
        scanner: "sandbox-audit",
        available: true,
        version: "1.0.0",
    };
}
/**
 * Get sandbox audit summary
 */
export function getSandboxAuditSummary(result) {
    const summaryFinding = result.findings.find((f) => f.ruleId === "sandbox-audit:summary");
    if (!summaryFinding?.metadata) {
        return null;
    }
    const meta = summaryFinding.metadata;
    return {
        totalFindings: meta.totalFindings || 0,
        byType: meta.byType || {},
        bySeverity: meta.bySeverity || {
            critical: 0,
            high: 0,
            medium: 0,
            low: 0,
            info: 0,
        },
    };
}
//# sourceMappingURL=sandbox-audit.js.map