/**
 * MCP Manifest Audit Scanner
 *
 * Parses MCP server manifests (server.json, mcp.json) and flags security issues:
 * - Missing destructiveHint on write/delete tools
 * - Missing readOnlyHint on read-only tools
 * - Missing inputSchema (allows arbitrary input)
 * - Unbounded allowedOrigins (wildcard *)
 * - Missing tool descriptions
 * - Excessive or undeclared permissions
 *
 * @module scanners/agent/manifest-audit
 */
import { createHash } from "crypto";
import { AGENT_SEVERITY_MAPPINGS } from "./types.js";
// ============================================================================
// Tool Classification Patterns
// ============================================================================
/**
 * Patterns indicating a tool performs write/destructive operations
 */
const DESTRUCTIVE_PATTERNS = [
    /write/i,
    /create/i,
    /delete/i,
    /remove/i,
    /update/i,
    /modify/i,
    /edit/i,
    /insert/i,
    /drop/i,
    /truncate/i,
    /execute/i,
    /run/i,
    /send/i,
    /post/i,
    /put/i,
    /patch/i,
    /push/i,
    /commit/i,
    /deploy/i,
    /install/i,
    /uninstall/i,
    /upload/i,
    /move/i,
    /rename/i,
    /chmod/i,
    /chown/i,
    /kill/i,
    /terminate/i,
    /restart/i,
    /stop/i,
    /start/i,
];
/**
 * Patterns indicating a tool only reads data
 */
const READONLY_PATTERNS = [
    /^read/i,
    /^get/i,
    /^fetch/i,
    /^list/i,
    /^search/i,
    /^find/i,
    /^query/i,
    /^show/i,
    /^view/i,
    /^describe/i,
    /^inspect/i,
    /^check/i,
    /^validate/i,
    /^verify/i,
    /^status/i,
    /^info/i,
    /^metadata/i,
    /^count/i,
    /^exists/i,
    /^has/i,
];
/**
 * Patterns indicating network access
 */
const NETWORK_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,
];
/**
 * Patterns indicating code execution
 */
const CODE_EXECUTION_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,
];
// ============================================================================
// Tool Classification Functions
// ============================================================================
/**
 * Check if a tool appears to be destructive based on name and description
 */
function isLikelyDestructive(tool) {
    const text = `${tool.name} ${tool.description || ""}`.toLowerCase();
    return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(text));
}
/**
 * Check if a tool appears to be read-only based on name and description
 */
function isLikelyReadOnly(tool) {
    const text = `${tool.name} ${tool.description || ""}`.toLowerCase();
    // If it matches destructive patterns, it's not read-only
    if (DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(text))) {
        return false;
    }
    return READONLY_PATTERNS.some((pattern) => pattern.test(text));
}
/**
 * Check if a tool appears to have network access
 */
function hasLikelyNetworkAccess(tool) {
    const text = `${tool.name} ${tool.description || ""}`.toLowerCase();
    return NETWORK_PATTERNS.some((pattern) => pattern.test(text));
}
/**
 * Check if a tool appears to execute code
 */
function hasLikelyCodeExecution(tool) {
    const text = `${tool.name} ${tool.description || ""}`.toLowerCase();
    return CODE_EXECUTION_PATTERNS.some((pattern) => pattern.test(text));
}
// ============================================================================
// Audit Checks
// ============================================================================
/**
 * Check for missing destructiveHint on likely destructive tools.
 * Only flags tools where destructiveHint is undefined (undeclared).
 * Explicit false declarations are respected as intentional.
 */
function checkMissingDestructiveHint(tool) {
    // Only flag if destructiveHint is undefined - respect explicit false
    if (isLikelyDestructive(tool) && tool.destructiveHint === undefined) {
        return {
            check: "missing-destructive-hint",
            severity: AGENT_SEVERITY_MAPPINGS["missing-destructive-hint"],
            tool: tool.name,
            description: `Tool "${tool.name}" appears to perform write/destructive operations but does not declare destructiveHint`,
            suggestion: `Add "destructiveHint": true or "destructiveHint": false to explicitly declare destructive capability`,
            currentValue: tool.destructiveHint,
            expectedValue: "boolean (true or false)",
        };
    }
    return null;
}
/**
 * Check for missing readOnlyHint on likely read-only tools.
 * Only flags tools where readOnlyHint is undefined (undeclared).
 * Explicit false declarations are respected as intentional.
 */
function checkMissingReadOnlyHint(tool) {
    // Only flag if readOnlyHint is undefined - respect explicit false
    if (isLikelyReadOnly(tool) && tool.readOnlyHint === undefined) {
        return {
            check: "missing-readonly-hint",
            severity: AGENT_SEVERITY_MAPPINGS["missing-readonly-hint"],
            tool: tool.name,
            description: `Tool "${tool.name}" appears to be read-only but does not declare readOnlyHint`,
            suggestion: `Add "readOnlyHint": true or "readOnlyHint": false to explicitly declare read-only capability`,
            currentValue: tool.readOnlyHint,
            expectedValue: "boolean (true or false)",
        };
    }
    return null;
}
/**
 * Check for missing input schema
 */
function checkMissingInputSchema(tool) {
    if (!tool.inputSchema) {
        return {
            check: "missing-input-schema",
            severity: AGENT_SEVERITY_MAPPINGS["missing-input-schema"],
            tool: tool.name,
            description: `Tool "${tool.name}" does not define an inputSchema, allowing arbitrary unvalidated input`,
            suggestion: `Define an inputSchema using JSON Schema or Zod to validate tool inputs and prevent injection attacks`,
            currentValue: undefined,
            expectedValue: "{ type: 'object', properties: {...}, required: [...] }",
        };
    }
    return null;
}
/**
 * Check for unbounded allowedOrigins (wildcard *)
 */
function checkUnboundedOrigins(tool) {
    if (tool.allowedOrigins?.includes("*")) {
        return {
            check: "unbounded-origins",
            severity: AGENT_SEVERITY_MAPPINGS["unbounded-origins"],
            tool: tool.name,
            description: `Tool "${tool.name}" allows requests from any origin (*), which could enable cross-origin attacks`,
            suggestion: `Replace wildcard (*) with specific allowed origins to limit cross-origin access`,
            currentValue: tool.allowedOrigins,
            expectedValue: ["https://trusted-domain.com"],
        };
    }
    return null;
}
/**
 * Check for missing tool description
 */
function checkMissingDescription(tool) {
    if (!tool.description || tool.description.trim().length < 10) {
        return {
            check: "missing-description",
            severity: AGENT_SEVERITY_MAPPINGS["missing-description"],
            tool: tool.name,
            description: `Tool "${tool.name}" has no description or a very short description, making it difficult for AI models to use safely`,
            suggestion: `Add a clear, detailed description explaining what the tool does, what inputs it expects, and any side effects`,
            currentValue: tool.description,
            expectedValue: "A clear description of at least 10 characters explaining the tool's purpose",
        };
    }
    return null;
}
/**
 * Check for undeclared network access
 * Only flags tools where networkAccess is undefined (undeclared).
 * Explicit false declarations are respected as intentional.
 */
function checkUndeclaredNetwork(tool) {
    // Only flag if networkAccess is undefined - respect explicit false
    if (hasLikelyNetworkAccess(tool) && tool.networkAccess === undefined) {
        return {
            check: "undeclared-network",
            severity: AGENT_SEVERITY_MAPPINGS["undeclared-network"],
            tool: tool.name,
            description: `Tool "${tool.name}" appears to have network access but does not declare networkAccess`,
            suggestion: `Add "networkAccess": true or "networkAccess": false to explicitly declare network capability`,
            currentValue: tool.networkAccess,
            expectedValue: "boolean (true or false)",
        };
    }
    return null;
}
/**
 * Check for undeclared code execution
 * Only flags tools where codeExecution is undefined (undeclared).
 * Explicit false declarations are respected as intentional.
 */
function checkUndeclaredCodeExecution(tool) {
    // Only flag if codeExecution is undefined - respect explicit false
    if (hasLikelyCodeExecution(tool) && tool.codeExecution === undefined) {
        return {
            check: "undeclared-code-execution",
            severity: AGENT_SEVERITY_MAPPINGS["undeclared-code-execution"],
            tool: tool.name,
            description: `Tool "${tool.name}" appears to execute code but does not declare codeExecution`,
            suggestion: `Add "codeExecution": true or "codeExecution": false to explicitly declare code execution capability`,
            currentValue: tool.codeExecution,
            expectedValue: "boolean (true or false)",
        };
    }
    return null;
}
// ============================================================================
// Main Scanner Function
// ============================================================================
/**
 * Run manifest audit on an MCP manifest
 */
export async function runManifestAudit(manifest, options) {
    const startTime = Date.now();
    const auditFindings = [];
    // Define all checks
    const allChecks = [
        { check: "missing-destructive-hint", fn: checkMissingDestructiveHint },
        { check: "missing-readonly-hint", fn: checkMissingReadOnlyHint },
        { check: "missing-input-schema", fn: checkMissingInputSchema },
        { check: "unbounded-origins", fn: checkUnboundedOrigins },
        { check: "missing-description", fn: checkMissingDescription },
        { check: "undeclared-network", fn: checkUndeclaredNetwork },
        { check: "undeclared-code-execution", fn: checkUndeclaredCodeExecution },
    ];
    // Filter checks based on options
    let checksToRun = allChecks;
    if (options?.onlyChecks && options.onlyChecks.length > 0) {
        checksToRun = allChecks.filter((c) => options.onlyChecks.includes(c.check));
    }
    else if (options?.skipChecks && options.skipChecks.length > 0) {
        checksToRun = allChecks.filter((c) => !options.skipChecks.includes(c.check));
    }
    // Run checks on each tool
    for (const tool of manifest.tools) {
        for (const { fn } of checksToRun) {
            const finding = fn(tool);
            if (finding) {
                auditFindings.push(finding);
            }
        }
    }
    // Convert audit findings to deterministic findings
    const findings = auditFindings.map((af, index) => ({
        scanner: "semgrep", // Map to existing type for compatibility
        ruleId: `manifest-audit:${af.check}`,
        file: "mcp-manifest",
        line: 0,
        message: af.description,
        severity: af.severity,
        confidence: 100,
        evidence: `Tool: ${af.tool}\nCurrent: ${JSON.stringify(af.currentValue)}\nExpected: ${JSON.stringify(af.expectedValue)}`,
        metadata: {
            agentScanner: "manifest-audit",
            check: af.check,
            tool: af.tool,
            suggestion: af.suggestion,
        },
    }));
    // Calculate manifest hash
    const manifestHash = createHash("sha256")
        .update(JSON.stringify(manifest))
        .digest("hex");
    return {
        scanner: "manifest-audit",
        findings,
        duration: Date.now() - startTime,
        success: true,
        mcpServerName: manifest.name,
        mcpServerVersion: manifest.version,
        manifestHash,
        version: "1.0.0",
        rulesUsed: checksToRun.map((c) => c.check),
    };
}
/**
 * Check if manifest audit is available (always true - no external deps)
 */
export async function checkManifestAuditAvailable() {
    return {
        scanner: "manifest-audit",
        available: true,
        version: "1.0.0",
    };
}
/**
 * Get summary statistics from audit findings
 */
export function getAuditSummary(result) {
    const byCheck = {
        "missing-destructive-hint": 0,
        "missing-readonly-hint": 0,
        "missing-input-schema": 0,
        "unbounded-origins": 0,
        "version-drift": 0,
        "missing-description": 0,
        "excessive-permissions": 0,
        "undeclared-network": 0,
        "undeclared-code-execution": 0,
    };
    const bySeverity = {
        critical: 0,
        high: 0,
        medium: 0,
        low: 0,
        info: 0,
    };
    const toolsWithFindings = new Set();
    for (const finding of result.findings) {
        const check = finding.ruleId.replace("manifest-audit:", "");
        if (check in byCheck) {
            byCheck[check]++;
        }
        bySeverity[finding.severity]++;
        const toolName = finding.metadata?.tool;
        if (toolName) {
            toolsWithFindings.add(toolName);
        }
    }
    // Estimate tools audited from rulesUsed count (approximate)
    const toolsAudited = Math.max(1, toolsWithFindings.size);
    const maxPossibleFindings = toolsAudited * (result.rulesUsed?.length || 7);
    const passRate = maxPossibleFindings > 0
        ? Math.round(((maxPossibleFindings - result.findings.length) / maxPossibleFindings) * 100)
        : 100;
    return {
        totalFindings: result.findings.length,
        byCheck,
        bySeverity,
        toolsAudited,
        passRate,
    };
}
//# sourceMappingURL=manifest-audit.js.map