// engine/supply-chain/dependency-checker.ts — SPEC-046
// Detects dependency confusion risks, lock file presence, and signing hygiene.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
// Lock files per ecosystem
const LOCK_FILES = [
    'package-lock.json',
    'pnpm-lock.yaml',
    'yarn.lock',
    'Pipfile.lock',
    'poetry.lock',
    'uv.lock',
    'go.sum',
    'Cargo.lock',
];
const DEP_CONFUSION_REFS = [
    'https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610',
    'https://owasp.org/www-project-top-ten/2021/A06_2021-Vulnerable_and_Outdated_Components',
    'https://docs.npmjs.com/cli/v9/using-npm/package-lock-json',
];
/** Detect if a lock file is present in the project. */
async function detectLockFile(projectPath) {
    for (const lockFile of LOCK_FILES) {
        try {
            await readFile(join(projectPath, lockFile), 'utf-8');
            return lockFile;
        }
        catch {
            // File not found, continue
        }
    }
    return null;
}
/** Check if lock file has integrity hashes (npm package-lock.json or pnpm-lock.yaml). */
async function lockFileHasHashes(projectPath, lockFile) {
    if (!lockFile.includes('package-lock') && !lockFile.includes('pnpm-lock')) {
        // For other lock files we can't easily verify hash presence without parsing
        return true; // Assume pinned for yarn.lock, poetry.lock, go.sum, Cargo.lock
    }
    try {
        const content = await readFile(join(projectPath, lockFile), 'utf-8');
        return content.includes('integrity') || content.includes('sha512') || content.includes('sha1');
    }
    catch {
        return false;
    }
}
/** Check for .npmrc with registry configuration. */
async function hasNpmrcRegistryConfig(projectPath) {
    try {
        const content = await readFile(join(projectPath, '.npmrc'), 'utf-8');
        return content.includes(':registry=') || content.includes('registry=');
    }
    catch {
        return false;
    }
}
/** Check if pip.conf or similar Python registry config exists. */
async function hasPythonRegistryConfig(projectPath) {
    const configs = ['pip.conf', '.pip/pip.conf', 'pyproject.toml'];
    for (const cfg of configs) {
        try {
            const content = await readFile(join(projectPath, cfg), 'utf-8');
            if (content.includes('index-url') ||
                content.includes('extra-index-url') ||
                content.includes('[[tool.uv.index]]')) {
                return true;
            }
        }
        catch {
            // File not found
        }
    }
    return false;
}
/** Detect unscoped private npm packages (potential confusion targets). */
async function detectUnscopedPrivatePackages(projectPath) {
    try {
        const content = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const pkg = JSON.parse(content);
        const name = pkg.name ?? '';
        const isScoped = name.startsWith('@');
        const isPrivate = pkg.private === true;
        const hasPublishConfig = pkg.publishConfig?.registry !== undefined;
        // Risk: non-scoped package that is not explicitly private and has no publishConfig
        if (!isScoped && !isPrivate && !hasPublishConfig) {
            return { count: 1, hasPublishConfig: false };
        }
        return { count: 0, hasPublishConfig };
    }
    catch {
        return { count: 0, hasPublishConfig: false };
    }
}
/**
 * Check for dependency confusion risks and lock file hygiene.
 */
export async function checkDependencies(projectPath) {
    const lockFile = await detectLockFile(projectPath);
    const hasHashes = lockFile ? await lockFileHasHashes(projectPath, lockFile) : false;
    const [hasNpmrc, hasPyConf, unscopedInfo] = await Promise.all([
        hasNpmrcRegistryConfig(projectPath),
        hasPythonRegistryConfig(projectPath),
        detectUnscopedPrivatePackages(projectPath),
    ]);
    const hasRegistryConfig = hasNpmrc || hasPyConf;
    // Assess dependency confusion risk
    // Only flag risk when there are unscoped private packages without registry binding
    let risk = 'none';
    if (!lockFile) {
        risk = 'critical';
    }
    else if (unscopedInfo.count > 0 && !hasRegistryConfig) {
        risk = 'high';
    }
    else if (unscopedInfo.count > 0) {
        risk = 'low';
    }
    return {
        lockFile,
        lockFileHasHashes: hasHashes,
        dependencyConfusionRisk: risk,
        hasRegistryConfig,
        unscopedPrivateCount: unscopedInfo.count,
    };
}
/**
 * Build dependency-related gaps from check results.
 */
export function buildDependencyGaps(result) {
    const gaps = [];
    if (!result.lockFile) {
        gaps.push({
            category: 'lockfile',
            severity: 'critical',
            message: 'No lock file detected — builds are not reproducible and are vulnerable to supply chain attacks',
            fix: 'Commit a lock file (package-lock.json, pnpm-lock.yaml, poetry.lock, go.sum, Cargo.lock)',
            references: [DEP_CONFUSION_REFS[2] ?? ''],
        });
    }
    else if (!result.lockFileHasHashes && result.lockFile.includes('package-lock')) {
        gaps.push({
            category: 'lockfile',
            severity: 'warning',
            message: 'Lock file found but integrity hashes are missing — packages not hash-pinned',
            fix: 'Regenerate the lock file with `npm install` or `pnpm install` to include integrity fields',
            references: [DEP_CONFUSION_REFS[2] ?? ''],
        });
    }
    if (result.dependencyConfusionRisk === 'critical' || result.dependencyConfusionRisk === 'high') {
        gaps.push({
            category: 'dependency-confusion',
            severity: result.dependencyConfusionRisk === 'critical' ? 'critical' : 'warning',
            message: `Dependency confusion risk: ${result.dependencyConfusionRisk} — unscoped package names without explicit registry binding`,
            fix: 'Use scoped packages (@company/pkg) and add registry config in .npmrc or pip.conf',
            references: DEP_CONFUSION_REFS.slice(0, 2),
        });
    }
    return gaps;
}
//# sourceMappingURL=dependency-checker.js.map