/**
 * Exfiltration Path Graph Scanner
 *
 * Builds a directed graph of MCP tools based on their capabilities,
 * then finds potential data exfiltration paths from secret sources
 * (tools that read sensitive data) to network sinks (tools that can
 * send data externally).
 *
 * Key features:
 * - Tool capability classification (reads_secrets, network_access, etc.)
 * - Path finding from sources to sinks
 * - Minimal cut-set computation (tools to sandbox)
 * - Mermaid diagram generation for visualization
 *
 * @module scanners/agent/exfil-path-graph
 */
import { createHash } from "crypto";
// ============================================================================
// Tool Classification Patterns
// ============================================================================
/**
 * Patterns indicating a tool reads secrets/credentials
 */
const READS_SECRETS_PATTERNS = [
    /secret/i,
    /credential/i,
    /password/i,
    /api[_\s-]?key/i,
    /token/i,
    /auth/i,
    /private[_\s-]?key/i,
    /access[_\s-]?key/i,
    /env(ironment)?/i,
    /config(uration)?/i,
    /\.env/i,
    /keychain/i,
    /vault/i,
    /1password/i,
    /lastpass/i,
    /bitwarden/i,
];
/**
 * Patterns indicating a tool reads files
 */
const READS_FILES_PATTERNS = [
    /read[_\s-]?file/i,
    /get[_\s-]?file/i,
    /load[_\s-]?file/i,
    /open[_\s-]?file/i,
    /file[_\s-]?content/i,
    /cat\s/i,
    /less\s/i,
    /head\s/i,
    /tail\s/i,
    /grep/i,
    /search[_\s-]?file/i,
    /glob/i,
    /directory/i,
    /folder/i,
    /path/i,
    /fs\./i,
    /filesystem/i,
];
/**
 * Patterns indicating a tool reads environment variables
 */
const READS_ENV_PATTERNS = [
    /env(ironment)?/i,
    /process\.env/i,
    /getenv/i,
    /environ/i,
    /variable/i,
    /dotenv/i,
];
/**
 * Patterns indicating a tool reads from database
 */
const READS_DATABASE_PATTERNS = [
    /database/i,
    /db/i,
    /sql/i,
    /query/i,
    /select/i,
    /table/i,
    /postgres/i,
    /mysql/i,
    /mongo/i,
    /redis/i,
    /supabase/i,
    /prisma/i,
    /drizzle/i,
];
/**
 * Patterns indicating a tool has network access
 */
const NETWORK_ACCESS_PATTERNS = [
    /http/i,
    /https/i,
    /url/i,
    /fetch/i,
    /request/i,
    /api/i,
    /webhook/i,
    /socket/i,
    /connect/i,
    /download/i,
    /upload/i,
    /stream/i,
    /remote/i,
    /external/i,
    /endpoint/i,
    /curl/i,
    /axios/i,
    /got\s/i,
    /node-fetch/i,
];
/**
 * Patterns indicating a tool sends email
 */
const SENDS_EMAIL_PATTERNS = [
    /email/i,
    /mail/i,
    /smtp/i,
    /sendgrid/i,
    /mailgun/i,
    /postmark/i,
    /ses\s/i,
    /newsletter/i,
];
/**
 * Patterns indicating a tool sends to webhooks
 */
const SENDS_WEBHOOK_PATTERNS = [
    /webhook/i,
    /callback/i,
    /notify/i,
    /slack/i,
    /discord/i,
    /teams/i,
    /trigger/i,
    /zapier/i,
    /ifttt/i,
];
/**
 * Patterns indicating a tool executes code
 */
const EXECUTES_CODE_PATTERNS = [
    /exec/i,
    /execute/i,
    /run/i,
    /eval/i,
    /shell/i,
    /bash/i,
    /command/i,
    /script/i,
    /spawn/i,
    /process/i,
    /terminal/i,
    /repl/i,
    /interpret/i,
    /compile/i,
];
/**
 * Patterns indicating a tool writes files
 */
const WRITES_FILES_PATTERNS = [
    /write[_\s-]?file/i,
    /save[_\s-]?file/i,
    /create[_\s-]?file/i,
    /update[_\s-]?file/i,
    /edit[_\s-]?file/i,
    /modify[_\s-]?file/i,
    /append/i,
    /overwrite/i,
    /delete[_\s-]?file/i,
    /remove[_\s-]?file/i,
];
/**
 * Patterns indicating a tool writes to database
 */
const WRITES_DATABASE_PATTERNS = [
    /insert/i,
    /update/i,
    /delete/i,
    /upsert/i,
    /create.*record/i,
    /modify.*record/i,
    /write.*db/i,
    /write.*database/i,
];
// ============================================================================
// Tool Classification Functions
// ============================================================================
/**
 * Classify a tool's capabilities based on name and description
 */
function classifyTool(tool) {
    const capabilities = [];
    const text = `${tool.name} ${tool.description || ""}`.toLowerCase();
    // Check each capability type
    if (READS_SECRETS_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("reads_secrets");
    }
    if (READS_FILES_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("reads_files");
    }
    if (READS_ENV_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("reads_env");
    }
    if (READS_DATABASE_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("reads_database");
    }
    if (WRITES_FILES_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("writes_files");
    }
    if (WRITES_DATABASE_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("writes_database");
    }
    if (NETWORK_ACCESS_PATTERNS.some((p) => p.test(text)) || tool.networkAccess) {
        capabilities.push("network_access");
    }
    if (EXECUTES_CODE_PATTERNS.some((p) => p.test(text)) || tool.codeExecution) {
        capabilities.push("executes_code");
    }
    if (tool.destructiveHint) {
        capabilities.push("modifies_state");
    }
    if (SENDS_EMAIL_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("sends_email");
    }
    if (SENDS_WEBHOOK_PATTERNS.some((p) => p.test(text))) {
        capabilities.push("sends_webhook");
    }
    if (NETWORK_ACCESS_PATTERNS.some((p) => p.test(text)) && !capabilities.includes("network_access")) {
        capabilities.push("accesses_external_api");
    }
    return capabilities;
}
/**
 * Calculate risk score for a tool based on capabilities
 */
function calculateToolRiskScore(capabilities) {
    const riskWeights = {
        reads_secrets: 30,
        reads_files: 15,
        reads_env: 25,
        reads_database: 20,
        writes_files: 20,
        writes_database: 25,
        network_access: 35,
        executes_code: 40,
        modifies_state: 15,
        sends_email: 25,
        sends_webhook: 30,
        accesses_external_api: 25,
    };
    let score = 0;
    for (const cap of capabilities) {
        score += riskWeights[cap] || 0;
    }
    return Math.min(100, score);
}
/**
 * Check if a tool is a potential secret source
 */
function isSecretSource(capabilities) {
    const secretSourceCaps = [
        "reads_secrets",
        "reads_env",
        "reads_files",
        "reads_database",
        "executes_code", // Can access anything
    ];
    return capabilities.some((c) => secretSourceCaps.includes(c));
}
/**
 * Check if a tool is a potential network sink
 */
function isNetworkSink(capabilities) {
    const networkCaps = [
        "network_access",
        "sends_email",
        "sends_webhook",
        "accesses_external_api",
        "executes_code", // Can make network calls
    ];
    return capabilities.some((c) => networkCaps.includes(c));
}
// ============================================================================
// Graph Building
// ============================================================================
/**
 * Build tool nodes from manifest
 */
function buildNodes(manifest) {
    const nodes = new Map();
    for (const tool of manifest.tools) {
        const capabilities = classifyTool(tool);
        const riskScore = calculateToolRiskScore(capabilities);
        nodes.set(tool.name, {
            name: tool.name,
            description: tool.description,
            capabilities,
            riskScore,
            isSecretSource: isSecretSource(capabilities),
            isNetworkSink: isNetworkSink(capabilities),
        });
    }
    return nodes;
}
/**
 * Build edges representing potential data flows
 *
 * In MCP, tools can be chained by the AI agent, so we model
 * potential data flows between any source and any sink.
 */
function buildEdges(nodes) {
    const edges = [];
    const nodeArray = Array.from(nodes.values());
    // Create implicit edges from sources to sinks
    // (data can flow through AI orchestration)
    for (const source of nodeArray) {
        if (!source.isSecretSource)
            continue;
        for (const target of nodeArray) {
            if (source.name === target.name)
                continue;
            if (!target.isNetworkSink)
                continue;
            edges.push({
                source: source.name,
                target: target.name,
                dataFlow: "implicit",
                description: `Data can flow from ${source.name} to ${target.name} via AI orchestration`,
            });
        }
    }
    // Add chained edges for code execution tools
    // (they can invoke any other tool)
    for (const tool of nodeArray) {
        if (tool.capabilities.includes("executes_code")) {
            for (const other of nodeArray) {
                if (tool.name === other.name)
                    continue;
                // Execution tools can read from anything
                if (other.isSecretSource && !tool.isSecretSource) {
                    edges.push({
                        source: other.name,
                        target: tool.name,
                        dataFlow: "chained",
                        description: `${tool.name} can execute code that reads from ${other.name}`,
                    });
                }
                // Execution tools can write to anything
                if (other.isNetworkSink && !tool.isNetworkSink) {
                    edges.push({
                        source: tool.name,
                        target: other.name,
                        dataFlow: "chained",
                        description: `${tool.name} can execute code that sends data via ${other.name}`,
                    });
                }
            }
        }
    }
    return edges;
}
// ============================================================================
// Path Finding
// ============================================================================
/**
 * Find all exfiltration paths from sources to sinks
 */
function findExfilPaths(nodes, edges) {
    const paths = [];
    // Build adjacency list
    const adjacency = new Map();
    for (const edge of edges) {
        if (!adjacency.has(edge.source)) {
            adjacency.set(edge.source, []);
        }
        adjacency.get(edge.source).push(edge.target);
    }
    // Find all sources and sinks
    const sources = Array.from(nodes.values()).filter((n) => n.isSecretSource);
    const sinks = Array.from(nodes.values()).filter((n) => n.isNetworkSink);
    // BFS to find paths from each source to each sink
    for (const source of sources) {
        for (const sink of sinks) {
            if (source.name === sink.name) {
                // Direct exfil - tool both reads secrets AND has network access
                paths.push({
                    source: source.name,
                    sink: sink.name,
                    path: [],
                    fullPath: [source.name],
                    riskLevel: "critical",
                    description: `Tool "${source.name}" can both read sensitive data and send it externally`,
                    mitigations: [
                        "Split into separate tools for reading and sending",
                        "Add explicit approval step before network operations",
                        "Implement data loss prevention (DLP) checks",
                    ],
                });
                continue;
            }
            // Find path using BFS
            const pathFound = bfsPath(source.name, sink.name, adjacency);
            if (pathFound) {
                const riskLevel = calculatePathRisk(pathFound, nodes);
                paths.push({
                    source: source.name,
                    sink: sink.name,
                    path: pathFound.slice(1, -1),
                    fullPath: pathFound,
                    riskLevel,
                    description: `Data can flow from "${source.name}" to "${sink.name}" via ${pathFound.length === 2 ? "direct connection" : `${pathFound.length - 2} intermediate tool(s)`}`,
                    mitigations: getMitigations(pathFound, nodes),
                });
            }
        }
    }
    return paths;
}
/**
 * BFS to find shortest path between two nodes
 */
function bfsPath(start, end, adjacency) {
    const visited = new Set();
    const queue = [
        { node: start, path: [start] },
    ];
    while (queue.length > 0) {
        const { node, path } = queue.shift();
        if (node === end) {
            return path;
        }
        if (visited.has(node))
            continue;
        visited.add(node);
        const neighbors = adjacency.get(node) || [];
        for (const neighbor of neighbors) {
            if (!visited.has(neighbor)) {
                queue.push({ node: neighbor, path: [...path, neighbor] });
            }
        }
    }
    return null;
}
/**
 * Calculate risk level for a path
 */
function calculatePathRisk(path, nodes) {
    if (path.length === 1) {
        return "critical"; // Single tool with both capabilities
    }
    let maxRisk = 0;
    for (const nodeName of path) {
        const node = nodes.get(nodeName);
        if (node) {
            maxRisk = Math.max(maxRisk, node.riskScore);
        }
    }
    if (maxRisk >= 60)
        return "critical";
    if (maxRisk >= 40)
        return "high";
    if (maxRisk >= 20)
        return "medium";
    return "low";
}
/**
 * Generate mitigations for a path
 */
function getMitigations(path, nodes) {
    const mitigations = [];
    if (path.length === 2) {
        mitigations.push("Add intermediate approval step before network operations");
        mitigations.push("Implement rate limiting on data transfer");
    }
    for (const nodeName of path) {
        const node = nodes.get(nodeName);
        if (node?.capabilities.includes("executes_code")) {
            mitigations.push(`Sandbox code execution in "${nodeName}" with restricted network access`);
        }
        if (node?.capabilities.includes("reads_secrets")) {
            mitigations.push(`Restrict "${nodeName}" to only necessary secrets using RBAC`);
        }
        if (node?.capabilities.includes("network_access")) {
            mitigations.push(`Add allowlist for "${nodeName}" network destinations`);
        }
    }
    // Add generic mitigations
    mitigations.push("Enable comprehensive audit logging");
    mitigations.push("Implement data classification and DLP policies");
    // Deduplicate
    return [...new Set(mitigations)];
}
// ============================================================================
// Cut Set Computation
// ============================================================================
/**
 * Find minimal cut set to break all exfiltration paths
 *
 * Uses a greedy approximation: repeatedly select the node that
 * appears in the most paths until all paths are cut.
 */
function findMinimalCutSet(paths, nodes) {
    if (paths.length === 0)
        return [];
    const cutSet = [];
    const remainingPaths = [...paths];
    while (remainingPaths.length > 0) {
        // Count node occurrences across all remaining paths
        const nodeCounts = new Map();
        for (const path of remainingPaths) {
            for (const node of path.fullPath) {
                nodeCounts.set(node, (nodeCounts.get(node) || 0) + 1);
            }
        }
        // Find node with highest count
        let maxNode = "";
        let maxCount = 0;
        for (const [node, count] of nodeCounts) {
            // Weight by risk score for tie-breaking
            const nodeData = nodes.get(node);
            const weightedCount = count + (nodeData ? nodeData.riskScore / 100 : 0);
            if (weightedCount > maxCount) {
                maxCount = weightedCount;
                maxNode = node;
            }
        }
        if (!maxNode)
            break;
        // Add to cut set
        cutSet.push(maxNode);
        // Remove paths containing this node
        for (let i = remainingPaths.length - 1; i >= 0; i--) {
            if (remainingPaths[i].fullPath.includes(maxNode)) {
                remainingPaths.splice(i, 1);
            }
        }
    }
    return cutSet;
}
// ============================================================================
// Mermaid Diagram Generation
// ============================================================================
/**
 * Generate Mermaid diagram of the tool graph
 */
function generateMermaidDiagram(graph) {
    const lines = ["graph LR"];
    // Add styling
    lines.push("  classDef source fill:#ff6b6b,stroke:#333,color:#fff");
    lines.push("  classDef sink fill:#4ecdc4,stroke:#333,color:#fff");
    lines.push("  classDef both fill:#ffe66d,stroke:#333,color:#000");
    lines.push("  classDef normal fill:#95e1d3,stroke:#333");
    lines.push("  classDef cutset fill:#ff9999,stroke:#f00,stroke-width:3px");
    // Add nodes
    for (const [name, node] of graph.nodes) {
        const sanitized = name.replace(/[^a-zA-Z0-9]/g, "_");
        const label = name.length > 20 ? name.slice(0, 17) + "..." : name;
        if (node.isSecretSource && node.isNetworkSink) {
            lines.push(`  ${sanitized}["${label}"]:::both`);
        }
        else if (node.isSecretSource) {
            lines.push(`  ${sanitized}["${label}"]:::source`);
        }
        else if (node.isNetworkSink) {
            lines.push(`  ${sanitized}["${label}"]:::sink`);
        }
        else {
            lines.push(`  ${sanitized}["${label}"]:::normal`);
        }
    }
    // Add edges for exfil paths only (to avoid clutter)
    const addedEdges = new Set();
    for (const path of graph.exfilPaths) {
        for (let i = 0; i < path.fullPath.length - 1; i++) {
            const from = path.fullPath[i].replace(/[^a-zA-Z0-9]/g, "_");
            const to = path.fullPath[i + 1].replace(/[^a-zA-Z0-9]/g, "_");
            const edgeKey = `${from}-${to}`;
            if (!addedEdges.has(edgeKey)) {
                addedEdges.add(edgeKey);
                const style = path.riskLevel === "critical" ? "==>" : "-->";
                lines.push(`  ${from} ${style} ${to}`);
            }
        }
    }
    // Mark cut set nodes
    for (const node of graph.cutSet) {
        const sanitized = node.replace(/[^a-zA-Z0-9]/g, "_");
        lines.push(`  ${sanitized}:::cutset`);
    }
    // Add legend
    lines.push("");
    lines.push("  subgraph Legend");
    lines.push("    source_leg[Secret Source]:::source");
    lines.push("    sink_leg[Network Sink]:::sink");
    lines.push("    both_leg[Source + Sink]:::both");
    lines.push("    cut_leg[Cut Set]:::cutset");
    lines.push("  end");
    return lines.join("\n");
}
// ============================================================================
// Main Scanner Function
// ============================================================================
/**
 * Run exfiltration path graph scanner
 */
export async function runExfilPathScanner(manifest, _options) {
    const startTime = Date.now();
    // Build graph
    const nodes = buildNodes(manifest);
    const edges = buildEdges(nodes);
    const exfilPaths = findExfilPaths(nodes, edges);
    const cutSet = findMinimalCutSet(exfilPaths, nodes);
    const graph = {
        nodes,
        edges,
        exfilPaths,
        cutSet,
        mermaidDiagram: "",
    };
    // Generate diagram
    graph.mermaidDiagram = generateMermaidDiagram(graph);
    // Convert to findings
    const findings = [];
    // Add finding for each critical/high path
    for (const path of exfilPaths) {
        if (path.riskLevel === "critical" || path.riskLevel === "high") {
            findings.push({
                scanner: "semgrep",
                ruleId: `exfil-path:${path.riskLevel}`,
                file: "mcp-manifest",
                line: 0,
                message: path.description,
                severity: path.riskLevel,
                confidence: 100,
                evidence: [
                    `Path: ${path.fullPath.join(" → ")}`,
                    `Mitigations:`,
                    ...path.mitigations.map((m) => `  - ${m}`),
                ].join("\n"),
                metadata: {
                    agentScanner: "exfil-path-graph",
                    source: path.source,
                    sink: path.sink,
                    pathLength: path.fullPath.length,
                    mitigations: path.mitigations,
                },
            });
        }
    }
    // Add summary finding
    if (exfilPaths.length > 0) {
        const criticalPaths = exfilPaths.filter((p) => p.riskLevel === "critical");
        const highPaths = exfilPaths.filter((p) => p.riskLevel === "high");
        findings.push({
            scanner: "semgrep",
            ruleId: "exfil-path:summary",
            file: "mcp-manifest",
            line: 0,
            message: `Found ${exfilPaths.length} potential exfiltration path(s): ${criticalPaths.length} critical, ${highPaths.length} high`,
            severity: criticalPaths.length > 0 ? "critical" : highPaths.length > 0 ? "high" : "medium",
            confidence: 100,
            evidence: [
                `Total paths: ${exfilPaths.length}`,
                `Secret sources: ${Array.from(nodes.values()).filter((n) => n.isSecretSource).length}`,
                `Network sinks: ${Array.from(nodes.values()).filter((n) => n.isNetworkSink).length}`,
                `Recommended cut set: ${cutSet.join(", ") || "None needed"}`,
                "",
                "Mermaid diagram:",
                "```mermaid",
                graph.mermaidDiagram,
                "```",
            ].join("\n"),
            metadata: {
                agentScanner: "exfil-path-graph",
                totalPaths: exfilPaths.length,
                criticalPaths: criticalPaths.length,
                highPaths: highPaths.length,
                cutSet,
                sources: Array.from(nodes.values())
                    .filter((n) => n.isSecretSource)
                    .map((n) => n.name),
                sinks: Array.from(nodes.values())
                    .filter((n) => n.isNetworkSink)
                    .map((n) => n.name),
            },
        });
    }
    // Calculate manifest hash
    const manifestHash = createHash("sha256")
        .update(JSON.stringify(manifest))
        .digest("hex");
    return {
        scanner: "exfil-path-graph",
        findings,
        duration: Date.now() - startTime,
        success: true,
        mcpServerName: manifest.name,
        mcpServerVersion: manifest.version,
        manifestHash,
        version: "1.0.0",
        rulesUsed: ["path-analysis"],
    };
}
/**
 * Check if exfil path scanner is available (always true - no external deps)
 */
export async function checkExfilPathAvailable() {
    return {
        scanner: "exfil-path-graph",
        available: true,
        version: "1.0.0",
    };
}
/**
 * Get exfil graph from scanner result
 */
export function getExfilGraph(result) {
    const summaryFinding = result.findings.find((f) => f.ruleId === "exfil-path:summary");
    if (!summaryFinding?.metadata) {
        return null;
    }
    const meta = summaryFinding.metadata;
    // Return partial graph info from metadata
    // (full graph requires re-running scanner)
    return {
        nodes: new Map(),
        edges: [],
        exfilPaths: [],
        cutSet: meta.cutSet || [],
        mermaidDiagram: extractMermaidDiagram(summaryFinding.evidence || ""),
    };
}
/**
 * Extract Mermaid diagram from evidence
 */
function extractMermaidDiagram(evidence) {
    const match = evidence.match(/```mermaid\n([\s\S]*?)\n```/);
    return match ? match[1] : "";
}
/**
 * Get exfil path summary
 */
export function getExfilSummary(result) {
    const summaryFinding = result.findings.find((f) => f.ruleId === "exfil-path:summary");
    if (!summaryFinding?.metadata) {
        return null;
    }
    const meta = summaryFinding.metadata;
    return {
        totalPaths: meta.totalPaths || 0,
        criticalPaths: meta.criticalPaths || 0,
        highPaths: meta.highPaths || 0,
        sources: meta.sources || [],
        sinks: meta.sinks || [],
        cutSet: meta.cutSet || [],
    };
}
//# sourceMappingURL=exfil-path-graph.js.map