/**
 * Tool Description Drift Scanner
 *
 * Detects changes to MCP tool definitions compared to a signed baseline.
 * This is a "rug-pull detector" that alerts when:
 * - Tool descriptions change (could indicate scope creep)
 * - Input schemas change (could introduce new attack vectors)
 * - Tools are added or removed
 * - Permissions/hints change
 *
 * @module scanners/agent/tool-description-drift
 */
import { createHash } from "crypto";
import { readFile, writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { AGENT_SEVERITY_MAPPINGS } from "./types.js";
// ============================================================================
// Hashing Functions
// ============================================================================
/**
 * Generate a hash for a tool definition
 */
function hashToolDefinition(tool) {
    const content = JSON.stringify({
        name: tool.name,
        description: tool.description,
        inputSchema: tool.inputSchema,
        destructiveHint: tool.destructiveHint,
        readOnlyHint: tool.readOnlyHint,
        networkAccess: tool.networkAccess,
        codeExecution: tool.codeExecution,
        requiredPermissions: tool.requiredPermissions,
    });
    return createHash("sha256").update(content).digest("hex").substring(0, 16);
}
/**
 * Generate a hash for just the input schema
 */
function hashInputSchema(schema) {
    const content = JSON.stringify(schema || {});
    return createHash("sha256").update(content).digest("hex").substring(0, 16);
}
/**
 * Generate a hash for the full manifest
 */
function hashManifest(manifest) {
    const content = JSON.stringify(manifest);
    return createHash("sha256").update(content).digest("hex");
}
// ============================================================================
// Baseline Management
// ============================================================================
/**
 * Create a baseline from a manifest
 */
export function createBaseline(manifest) {
    const tools = manifest.tools.map((tool) => ({
        name: tool.name,
        hash: hashToolDefinition(tool),
        description: tool.description,
        schemaHash: hashInputSchema(tool.inputSchema),
        hints: {
            destructive: tool.destructiveHint,
            readOnly: tool.readOnlyHint,
        },
        capturedAt: new Date().toISOString(),
    }));
    return {
        serverName: manifest.name,
        version: manifest.version,
        manifestHash: hashManifest(manifest),
        tools,
        signedAt: new Date().toISOString(),
    };
}
/**
 * Load baseline from file
 */
export async function loadBaseline(baselinesDir, serverName) {
    const baselinePath = join(baselinesDir, `${serverName}.json`);
    try {
        const content = await readFile(baselinePath, "utf-8");
        return JSON.parse(content);
    }
    catch {
        return null;
    }
}
/**
 * Save baseline to file
 */
export async function saveBaseline(baselinesDir, baseline) {
    await mkdir(baselinesDir, { recursive: true });
    const baselinePath = join(baselinesDir, `${baseline.serverName}.json`);
    await writeFile(baselinePath, JSON.stringify(baseline, null, 2));
}
// ============================================================================
// Drift Detection
// ============================================================================
/**
 * Compare current manifest against baseline and detect drift
 */
export function detectDrift(manifest, baseline) {
    const findings = [];
    const now = new Date().toISOString();
    const currentToolMap = new Map(manifest.tools.map((t) => [t.name, t]));
    const baselineToolMap = new Map(baseline.tools.map((t) => [t.name, t]));
    // Check for removed tools
    for (const baselineTool of baseline.tools) {
        if (!currentToolMap.has(baselineTool.name)) {
            findings.push({
                type: "tool-removed",
                severity: AGENT_SEVERITY_MAPPINGS["tool-removed"],
                tool: baselineTool.name,
                description: `Tool "${baselineTool.name}" was present in baseline but is now missing`,
                baselineValue: baselineTool.description,
                currentValue: undefined,
                detectedAt: now,
            });
        }
    }
    // Check for added tools and changes
    for (const tool of manifest.tools) {
        const baselineTool = baselineToolMap.get(tool.name);
        if (!baselineTool) {
            // New tool added
            findings.push({
                type: "tool-added",
                severity: AGENT_SEVERITY_MAPPINGS["tool-added"],
                tool: tool.name,
                description: `New tool "${tool.name}" was added since baseline`,
                baselineValue: undefined,
                currentValue: tool.description,
                detectedAt: now,
            });
            continue;
        }
        // Check for description change
        if (tool.description !== baselineTool.description) {
            findings.push({
                type: "description-changed",
                severity: AGENT_SEVERITY_MAPPINGS["description-changed"],
                tool: tool.name,
                description: `Tool "${tool.name}" description changed - may indicate scope change`,
                baselineValue: baselineTool.description,
                currentValue: tool.description,
                detectedAt: now,
            });
        }
        // Check for schema change
        const currentSchemaHash = hashInputSchema(tool.inputSchema);
        if (currentSchemaHash !== baselineTool.schemaHash) {
            findings.push({
                type: "schema-changed",
                severity: AGENT_SEVERITY_MAPPINGS["schema-changed"],
                tool: tool.name,
                description: `Tool "${tool.name}" input schema changed - may introduce new attack vectors`,
                baselineValue: `schema hash: ${baselineTool.schemaHash}`,
                currentValue: `schema hash: ${currentSchemaHash}`,
                detectedAt: now,
            });
        }
        // Check for permission/hint changes
        if (tool.destructiveHint !== baselineTool.hints.destructive ||
            tool.readOnlyHint !== baselineTool.hints.readOnly) {
            findings.push({
                type: "permission-changed",
                severity: AGENT_SEVERITY_MAPPINGS["permission-changed"],
                tool: tool.name,
                description: `Tool "${tool.name}" hints changed - may indicate capability change`,
                baselineValue: JSON.stringify(baselineTool.hints),
                currentValue: JSON.stringify({
                    destructive: tool.destructiveHint,
                    readOnly: tool.readOnlyHint,
                }),
                detectedAt: now,
            });
        }
        // Check for overall hash change (catches any other changes)
        const currentHash = hashToolDefinition(tool);
        if (currentHash !== baselineTool.hash) {
            // Only add if not already covered by specific checks above
            const hasSpecificFinding = findings.some((f) => f.tool === tool.name &&
                ["description-changed", "schema-changed", "permission-changed"].includes(f.type));
            if (!hasSpecificFinding) {
                findings.push({
                    type: "capability-changed",
                    severity: AGENT_SEVERITY_MAPPINGS["capability-changed"],
                    tool: tool.name,
                    description: `Tool "${tool.name}" definition changed in ways not covered by specific checks`,
                    baselineValue: `hash: ${baselineTool.hash}`,
                    currentValue: `hash: ${currentHash}`,
                    detectedAt: now,
                });
            }
        }
    }
    return findings;
}
// ============================================================================
// Main Scanner Function
// ============================================================================
/**
 * Run tool description drift scanner
 */
export async function runToolDriftScanner(manifest, options) {
    const startTime = Date.now();
    const baselinesDir = options?.baselinesDir || ".vaspera/agent/baselines";
    // Try to load existing baseline
    let baseline = await loadBaseline(baselinesDir, manifest.name);
    // Handle missing baseline
    if (!baseline) {
        if (options?.createIfMissing || options?.forceNewBaseline) {
            // Create and save new baseline
            baseline = createBaseline(manifest);
            await saveBaseline(baselinesDir, baseline);
            return {
                scanner: "tool-description-drift",
                findings: [],
                duration: Date.now() - startTime,
                success: true,
                mcpServerName: manifest.name,
                mcpServerVersion: manifest.version,
                manifestHash: baseline.manifestHash,
                version: "1.0.0",
                rulesUsed: ["baseline-created"],
            };
        }
        // No baseline and not creating - return info finding
        const findings = [
            {
                scanner: "semgrep",
                ruleId: "tool-drift:no-baseline",
                file: "mcp-manifest",
                line: 0,
                message: `No baseline found for "${manifest.name}". Run with createIfMissing: true to create one.`,
                severity: "info",
                confidence: 100,
                evidence: `Server: ${manifest.name}\nBaselines directory: ${baselinesDir}`,
                metadata: {
                    agentScanner: "tool-description-drift",
                    suggestion: "Create a baseline to enable drift detection",
                },
            },
        ];
        return {
            scanner: "tool-description-drift",
            findings,
            duration: Date.now() - startTime,
            success: true,
            mcpServerName: manifest.name,
            mcpServerVersion: manifest.version,
            version: "1.0.0",
            rulesUsed: ["no-baseline"],
        };
    }
    // Force new baseline if requested
    if (options?.forceNewBaseline) {
        baseline = createBaseline(manifest);
        await saveBaseline(baselinesDir, baseline);
        return {
            scanner: "tool-description-drift",
            findings: [],
            duration: Date.now() - startTime,
            success: true,
            mcpServerName: manifest.name,
            mcpServerVersion: manifest.version,
            manifestHash: baseline.manifestHash,
            version: "1.0.0",
            rulesUsed: ["baseline-updated"],
        };
    }
    // Detect drift
    let driftFindings = detectDrift(manifest, baseline);
    // Filter by type if specified
    if (options?.skipTypes && options.skipTypes.length > 0) {
        driftFindings = driftFindings.filter((f) => !options.skipTypes.includes(f.type));
    }
    // Convert to deterministic findings
    const findings = driftFindings.map((df) => ({
        scanner: "semgrep",
        ruleId: `tool-drift:${df.type}`,
        file: "mcp-manifest",
        line: 0,
        message: df.description,
        severity: df.severity,
        confidence: 100,
        evidence: `Tool: ${df.tool}\nBaseline: ${df.baselineValue || "N/A"}\nCurrent: ${df.currentValue || "N/A"}`,
        metadata: {
            agentScanner: "tool-description-drift",
            driftType: df.type,
            tool: df.tool,
            baselineDate: baseline.signedAt,
        },
    }));
    return {
        scanner: "tool-description-drift",
        findings,
        duration: Date.now() - startTime,
        success: true,
        mcpServerName: manifest.name,
        mcpServerVersion: manifest.version,
        manifestHash: hashManifest(manifest),
        version: "1.0.0",
        rulesUsed: ["baseline-comparison"],
    };
}
/**
 * Check if drift scanner is available (always true - no external deps)
 */
export async function checkToolDriftAvailable() {
    return {
        scanner: "tool-description-drift",
        available: true,
        version: "1.0.0",
    };
}
/**
 * Get summary of drift findings
 */
export function getDriftSummary(result) {
    const byType = {
        "tool-added": 0,
        "tool-removed": 0,
        "description-changed": 0,
        "schema-changed": 0,
        "permission-changed": 0,
        "capability-changed": 0,
    };
    const bySeverity = {
        critical: 0,
        high: 0,
        medium: 0,
        low: 0,
        info: 0,
    };
    const affectedTools = new Set();
    for (const finding of result.findings) {
        const type = finding.ruleId.replace("tool-drift:", "");
        if (type in byType) {
            byType[type]++;
        }
        bySeverity[finding.severity]++;
        const toolName = finding.metadata?.tool;
        if (toolName) {
            affectedTools.add(toolName);
        }
    }
    // Calculate baseline age if available
    let baselineAge;
    const baselineDate = result.findings[0]?.metadata?.baselineDate;
    if (baselineDate) {
        const baselineTime = new Date(baselineDate).getTime();
        const now = Date.now();
        baselineAge = Math.floor((now - baselineTime) / (1000 * 60 * 60 * 24));
    }
    return {
        totalDrift: result.findings.length,
        byType,
        bySeverity,
        affectedTools: Array.from(affectedTools),
        baselineAge,
    };
}
//# sourceMappingURL=tool-description-drift.js.map