"use strict";
/**
 * Layer 3: Package Hallucination Check (Story C - Hallucination Firewall)
 *
 * Verifies if imported packages actually exist and assesses their risk
 * Prevents typosquatting, dependency confusion, and AI-hallucinated packages
 *
 * Features:
 * - Registry metadata fetching (npm, PyPI)
 * - Risk score calculation based on multiple factors
 * - Typosquatting detection via Levenshtein distance
 * - Package age and popularity analysis
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.LEGITIMATE_PACKAGES = exports.POPULAR_PYTHON_PACKAGES = exports.POPULAR_NPM_PACKAGES = void 0;
exports.checkPackages = checkPackages;
exports.levenshteinDistance = levenshteinDistance;
exports.checkTyposquatting = checkTyposquatting;
exports.hasSuspiciousNamingPattern = hasSuspiciousNamingPattern;
exports.computeNPMRiskScore = computeNPMRiskScore;
exports.computePyPIRiskScore = computePyPIRiskScore;
const registry_clients_1 = require("../utils/registry-clients");
// ============================================================================
// Configuration
// ============================================================================
// Maximum packages to check per scan (cost/time control)
const MAX_PACKAGES_TO_CHECK = 50;
// ============================================================================
// Popular Packages for Typosquatting Detection
// ============================================================================
const POPULAR_NPM_PACKAGES = new Set([
    // Core frameworks
    'react', 'vue', 'angular', 'svelte', 'next', 'nuxt', 'gatsby',
    'express', 'fastify', 'koa', 'hapi', 'nest', 'nestjs',
    // Utilities
    'lodash', 'underscore', 'ramda', 'date-fns', 'dayjs', 'moment',
    'axios', 'node-fetch', 'got', 'request', 'superagent',
    // Build tools
    'webpack', 'rollup', 'vite', 'parcel', 'esbuild', 'swc',
    'babel', 'typescript', 'eslint', 'prettier', 'jest', 'vitest', 'mocha',
    // Database
    'mongoose', 'sequelize', 'prisma', 'typeorm', 'knex', 'pg', 'mysql', 'sqlite3',
    // Other popular
    'socket.io', 'ws', 'graphql', 'apollo', 'redux', 'mobx', 'zustand',
    'tailwindcss', 'styled-components', 'emotion', 'sass', 'postcss',
    'dotenv', 'cors', 'helmet', 'morgan', 'winston', 'pino',
    'uuid', 'crypto-js', 'bcrypt', 'jsonwebtoken', 'passport',
    'commander', 'yargs', 'inquirer', 'chalk', 'ora',
]);
exports.POPULAR_NPM_PACKAGES = POPULAR_NPM_PACKAGES;
const POPULAR_PYTHON_PACKAGES = new Set([
    'requests', 'flask', 'django', 'fastapi', 'numpy', 'pandas',
    'scipy', 'matplotlib', 'tensorflow', 'pytorch', 'torch', 'keras',
    'scikit-learn', 'sklearn', 'pillow', 'opencv-python', 'beautifulsoup4',
    'sqlalchemy', 'celery', 'redis', 'boto3', 'pytest', 'black', 'flake8',
    'pydantic', 'httpx', 'aiohttp', 'uvicorn', 'gunicorn',
]);
exports.POPULAR_PYTHON_PACKAGES = POPULAR_PYTHON_PACKAGES;
// ============================================================================
// Legitimate Packages (Skip checking)
// ============================================================================
const LEGITIMATE_PACKAGES = new Set([
    // Scoped packages from trusted orgs
    '@supabase/ssr', '@supabase/supabase-js', '@supabase/auth-helpers-nextjs',
    '@anthropic-ai/sdk', '@openai/openai', '@langchain/core', '@langchain/openai',
    '@octokit/rest', '@octokit/core',
    '@radix-ui/react-avatar', '@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu',
    '@radix-ui/react-scroll-area', '@radix-ui/react-slot', '@radix-ui/react-tabs',
    '@tailwindcss/postcss', '@tailwindcss/typography',
    '@types/node', '@types/react', '@types/react-dom',
    // Common packages with unusual names
    'class-variance-authority', 'clsx', 'tailwind-merge', 'cva',
    'lucide-react', 'next-themes', 'sonner', 'zod', 'zustand',
    'geist', 'sharp', 'turbo', 'tsup', 'tsx',
    // Known short names
    'ms', 'qs', 'ws', 'pg', 'ip', 'os', 'fs', 'vm',
]);
exports.LEGITIMATE_PACKAGES = LEGITIMATE_PACKAGES;
// ============================================================================
// Typosquatting Detection
// ============================================================================
/**
 * Calculate Levenshtein distance between two strings
 */
function levenshteinDistance(a, b) {
    const matrix = [];
    for (let i = 0; i <= b.length; i++) {
        matrix[i] = [i];
    }
    for (let j = 0; j <= a.length; j++) {
        matrix[0][j] = j;
    }
    for (let i = 1; i <= b.length; i++) {
        for (let j = 1; j <= a.length; j++) {
            if (b.charAt(i - 1) === a.charAt(j - 1)) {
                matrix[i][j] = matrix[i - 1][j - 1];
            }
            else {
                matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
            }
        }
    }
    return matrix[b.length][a.length];
}
/**
 * Check if package name is similar to a popular package (potential typosquat)
 */
function checkTyposquatting(packageName, ecosystem) {
    const name = packageName.toLowerCase();
    const popularPackages = ecosystem === 'npm' ? POPULAR_NPM_PACKAGES : POPULAR_PYTHON_PACKAGES;
    for (const popular of popularPackages) {
        // Skip if it's the actual package
        if (name === popular)
            continue;
        const distance = levenshteinDistance(name, popular);
        // Flag if 1-2 character difference and similar length
        if (distance === 1 && Math.abs(name.length - popular.length) <= 1) {
            return { isSimilar: true, similarTo: popular, distance };
        }
        if (distance === 2 && name.length >= 5 && Math.abs(name.length - popular.length) <= 1) {
            return { isSimilar: true, similarTo: popular, distance };
        }
    }
    return { isSimilar: false };
}
/**
 * Check for suspicious naming patterns
 */
function hasSuspiciousNamingPattern(packageName) {
    const suspiciousPatterns = [
        { pattern: /^[a-z]+-js$/, desc: 'package-js suffix (common typosquat pattern)' },
        { pattern: /^node-[a-z]{2,}$/, desc: 'node-package prefix' },
        { pattern: /^[a-z]+-node$/, desc: 'package-node suffix' },
        { pattern: /-\d{3,}$/, desc: 'ends with many numbers' },
        { pattern: /^[a-z]{1,2}-[a-z]+$/, desc: 'very short prefix' },
        { pattern: /[0o][0o]|[1l][1l]/i, desc: 'character substitution (0/o, 1/l)' },
    ];
    for (const { pattern, desc } of suspiciousPatterns) {
        if (pattern.test(packageName)) {
            return { suspicious: true, pattern: desc };
        }
    }
    return { suspicious: false };
}
// ============================================================================
// Risk Score Calculation
// ============================================================================
/**
 * Compute risk score for an npm package
 */
async function computeNPMRiskScore(dep, metadata) {
    const factors = [];
    let totalScore = 0;
    // Factor 1: Package not found (highest risk - likely hallucinated)
    if (!metadata) {
        factors.push({
            name: 'package_not_found',
            score: 100,
            description: 'Package does not exist in npm registry. Likely a hallucinated package name.',
        });
        return {
            package: dep.name,
            ecosystem: 'npm',
            totalScore: 100,
            factors,
            recommendation: 'block',
            severity: 'critical',
        };
    }
    // Factor 2: Package age
    const ageInDays = (0, registry_clients_1.calculatePackageAgeDays)(metadata.time?.created);
    if (ageInDays < 7) {
        factors.push({
            name: 'very_new_package',
            score: 30,
            description: `Package created ${ageInDays} days ago (< 7 days)`,
        });
        totalScore += 30;
    }
    else if (ageInDays < 30) {
        factors.push({
            name: 'new_package',
            score: 15,
            description: `Package created ${ageInDays} days ago (< 30 days)`,
        });
        totalScore += 15;
    }
    // Factor 3: Download count
    const weeklyDownloads = metadata.downloads?.weekly || 0;
    if (weeklyDownloads < 10) {
        factors.push({
            name: 'no_downloads',
            score: 25,
            description: `Only ${weeklyDownloads} weekly downloads`,
        });
        totalScore += 25;
    }
    else if (weeklyDownloads < 100) {
        factors.push({
            name: 'low_downloads',
            score: 15,
            description: `Only ${weeklyDownloads} weekly downloads (< 100)`,
        });
        totalScore += 15;
    }
    else if (weeklyDownloads < 1000) {
        factors.push({
            name: 'moderate_downloads',
            score: 5,
            description: `${weeklyDownloads} weekly downloads (< 1000)`,
        });
        totalScore += 5;
    }
    // Factor 4: Typosquatting similarity
    const typoCheck = checkTyposquatting(dep.name, 'npm');
    if (typoCheck.isSimilar && typoCheck.distance === 1) {
        factors.push({
            name: 'likely_typosquat',
            score: 40,
            description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`,
        });
        totalScore += 40;
    }
    else if (typoCheck.isSimilar && typoCheck.distance === 2) {
        factors.push({
            name: 'possible_typosquat',
            score: 20,
            description: `Name similar to popular package "${typoCheck.similarTo}" (${typoCheck.distance} char diff)`,
        });
        totalScore += 20;
    }
    // Factor 5: Suspicious naming pattern
    const namingCheck = hasSuspiciousNamingPattern(dep.name);
    if (namingCheck.suspicious) {
        factors.push({
            name: 'suspicious_name',
            score: 15,
            description: `Suspicious naming pattern: ${namingCheck.pattern}`,
        });
        totalScore += 15;
    }
    // Factor 6: No repository/homepage
    const hasRepo = !!metadata.repository?.url;
    const hasHomepage = !!metadata.homepage;
    if (!hasRepo && !hasHomepage) {
        factors.push({
            name: 'no_source_links',
            score: 15,
            description: 'Package has no repository or homepage link',
        });
        totalScore += 15;
    }
    // Factor 7: No description
    if (!metadata.description || metadata.description.length < 10) {
        factors.push({
            name: 'no_description',
            score: 10,
            description: 'Package has no meaningful description',
        });
        totalScore += 10;
    }
    // Factor 8: Single maintainer on new package
    const maintainerCount = metadata.maintainers?.length || 0;
    if (maintainerCount === 1 && ageInDays < 30) {
        factors.push({
            name: 'single_new_maintainer',
            score: 10,
            description: 'Single maintainer on a new package',
        });
        totalScore += 10;
    }
    // Determine recommendation and severity
    let recommendation;
    let severity;
    if (totalScore >= 70) {
        recommendation = 'block';
        severity = 'high';
    }
    else if (totalScore >= 40) {
        recommendation = 'review';
        severity = 'medium';
    }
    else if (totalScore >= 20) {
        recommendation = 'review';
        severity = 'low';
    }
    else {
        recommendation = 'allow';
        severity = 'info';
    }
    return {
        package: dep.name,
        ecosystem: 'npm',
        totalScore: Math.min(totalScore, 100),
        factors,
        recommendation,
        severity,
    };
}
/**
 * Compute risk score for a Python package
 */
async function computePyPIRiskScore(dep, metadata) {
    const factors = [];
    let totalScore = 0;
    // Factor 1: Package not found
    if (!metadata) {
        factors.push({
            name: 'package_not_found',
            score: 100,
            description: 'Package does not exist in PyPI registry. Likely a hallucinated package name.',
        });
        return {
            package: dep.name,
            ecosystem: 'python',
            totalScore: 100,
            factors,
            recommendation: 'block',
            severity: 'critical',
        };
    }
    // Factor 2: Typosquatting
    const typoCheck = checkTyposquatting(dep.name, 'python');
    if (typoCheck.isSimilar && typoCheck.distance === 1) {
        factors.push({
            name: 'likely_typosquat',
            score: 40,
            description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`,
        });
        totalScore += 40;
    }
    else if (typoCheck.isSimilar && typoCheck.distance === 2) {
        factors.push({
            name: 'possible_typosquat',
            score: 20,
            description: `Name similar to popular package "${typoCheck.similarTo}"`,
        });
        totalScore += 20;
    }
    // Factor 3: Suspicious naming
    const namingCheck = hasSuspiciousNamingPattern(dep.name);
    if (namingCheck.suspicious) {
        factors.push({
            name: 'suspicious_name',
            score: 15,
            description: `Suspicious naming pattern: ${namingCheck.pattern}`,
        });
        totalScore += 15;
    }
    // Factor 4: No project URLs
    const hasProjectUrls = metadata.projectUrls && Object.keys(metadata.projectUrls).length > 0;
    if (!hasProjectUrls) {
        factors.push({
            name: 'no_project_urls',
            score: 15,
            description: 'Package has no project URLs (repository, homepage, etc.)',
        });
        totalScore += 15;
    }
    // Factor 5: No summary/description
    if (!metadata.summary || metadata.summary.length < 10) {
        factors.push({
            name: 'no_description',
            score: 10,
            description: 'Package has no meaningful description',
        });
        totalScore += 10;
    }
    // Determine recommendation and severity
    let recommendation;
    let severity;
    if (totalScore >= 70) {
        recommendation = 'block';
        severity = 'high';
    }
    else if (totalScore >= 40) {
        recommendation = 'review';
        severity = 'medium';
    }
    else if (totalScore >= 20) {
        recommendation = 'review';
        severity = 'low';
    }
    else {
        recommendation = 'allow';
        severity = 'info';
    }
    return {
        package: dep.name,
        ecosystem: 'python',
        totalScore: Math.min(totalScore, 100),
        factors,
        recommendation,
        severity,
    };
}
// ============================================================================
// Vulnerability Generation
// ============================================================================
/**
 * Build description from risk score
 */
function buildRiskDescription(risk) {
    const factorList = risk.factors.map(f => `- ${f.description}`).join('\n');
    if (risk.totalScore >= 70) {
        return `Package "${risk.package}" has high risk indicators (score: ${risk.totalScore}/100):\n${factorList}\n\nThis may be a hallucinated package name or a typosquatting attempt.`;
    }
    if (risk.totalScore >= 40) {
        return `Package "${risk.package}" has moderate risk indicators (score: ${risk.totalScore}/100):\n${factorList}\n\nReview this dependency before using.`;
    }
    return `Package "${risk.package}" has some risk factors (score: ${risk.totalScore}/100):\n${factorList}`;
}
/**
 * Build suggested fix from risk score
 */
function buildRiskSuggestedFix(risk) {
    if (risk.factors.some(f => f.name === 'package_not_found')) {
        return 'Verify the package name is correct. This package does not exist in the registry - it may be a hallucinated name from an AI tool.';
    }
    if (risk.factors.some(f => f.name.includes('typosquat'))) {
        const typoFactor = risk.factors.find(f => f.name.includes('typosquat'));
        const match = typoFactor?.description.match(/"([^"]+)"/);
        const intendedPackage = match?.[1];
        return `Verify this is the intended package. Did you mean "${intendedPackage}"?`;
    }
    if (risk.totalScore >= 40) {
        return 'Review this package before using. Check the repository, maintainers, and recent activity.';
    }
    return 'Consider reviewing this package\'s repository and maintainers.';
}
// ============================================================================
// Main Check Function
// ============================================================================
/**
 * Check packages in a file for hallucination and risk indicators
 */
async function checkPackages(content, filePath) {
    const vulnerabilities = [];
    // Determine file type
    const fileType = (0, registry_clients_1.getPackageFileType)(filePath);
    if (!fileType) {
        return vulnerabilities;
    }
    // Extract dependencies based on file type
    let dependencies = [];
    if (fileType === 'npm' && filePath.endsWith('package.json')) {
        dependencies = (0, registry_clients_1.extractNpmDependencies)(content);
    }
    else if (fileType === 'python') {
        dependencies = (0, registry_clients_1.extractPythonRequirements)(content);
    }
    if (dependencies.length === 0) {
        return vulnerabilities;
    }
    const lines = content.split('\n');
    // Filter out legitimate/known packages and scoped packages
    const packagesToCheck = dependencies.filter(dep => {
        // Skip scoped packages (@org/package) - usually legitimate
        if (dep.name.startsWith('@'))
            return false;
        // Skip known legitimate packages
        if (LEGITIMATE_PACKAGES.has(dep.name))
            return false;
        // Skip exact matches to popular packages
        if (POPULAR_NPM_PACKAGES.has(dep.name.toLowerCase()))
            return false;
        if (POPULAR_PYTHON_PACKAGES.has(dep.name.toLowerCase()))
            return false;
        return true;
    });
    // Limit packages to check (cost control)
    const limitedPackages = packagesToCheck.slice(0, MAX_PACKAGES_TO_CHECK);
    // Check each package
    for (const dep of limitedPackages) {
        let risk;
        if (fileType === 'npm') {
            const metadata = await (0, registry_clients_1.fetchNPMMetadata)(dep.name);
            risk = await computeNPMRiskScore(dep, metadata);
        }
        else {
            const metadata = await (0, registry_clients_1.fetchPyPIMetadata)(dep.name);
            risk = await computePyPIRiskScore(dep, metadata);
        }
        // Only create vulnerabilities for packages that need attention
        if (risk.recommendation !== 'allow') {
            vulnerabilities.push({
                id: `pkg-risk-${filePath}-${dep.name}`,
                filePath,
                lineNumber: dep.line,
                lineContent: lines[dep.line - 1]?.trim() || dep.name,
                severity: risk.severity,
                category: 'suspicious_package',
                title: risk.totalScore >= 70
                    ? 'Potentially hallucinated dependency'
                    : 'Suspicious dependency',
                description: buildRiskDescription(risk),
                suggestedFix: buildRiskSuggestedFix(risk),
                confidence: risk.totalScore >= 70 ? 'high' : 'medium',
                layer: 3,
                requiresAIValidation: risk.totalScore < 70, // High-confidence issues don't need AI validation
            });
        }
        // Rate limiting between requests
        await (0, registry_clients_1.rateLimitDelay)();
    }
    return vulnerabilities;
}
//# sourceMappingURL=package-check.js.map