/**
 * SupplyChain -- Dependency and module integrity verification.
 *
 * Verifies npm dependencies, shared module hashes, and MCP server
 * integrity at agent startup. Addresses OWASP ASI04 (Supply Chain).
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { execFileSync } from 'child_process';
export class SupplyChain {
    config;
    constructor(config) {
        this.config = {
            projectRoot: config.projectRoot,
            sharedDir: config.sharedDir || path.join(config.projectRoot, 'shared'),
            toolRegistryPath: config.toolRegistryPath || path.join(config.projectRoot, 'TOOL_REGISTRY.md'),
            auditLogPath: config.auditLogPath || path.join(config.projectRoot, '.governance', 'audit', 'supply-chain.jsonl'),
            skipNpmAudit: config.skipNpmAudit ?? false,
        };
    }
    /**
     * Run a full supply chain audit. Returns pass/fail with details.
     */
    audit() {
        const failures = [];
        const vulnerabilities = this.checkVulnerabilities();
        if (vulnerabilities.critical > 0) {
            failures.push(`${vulnerabilities.critical} critical vulnerabilities found`);
        }
        const lockfileIntegrity = this.checkLockfile();
        if (!lockfileIntegrity.valid && lockfileIntegrity.exists) {
            failures.push('Lockfile integrity check failed');
        }
        const moduleIntegrity = this.checkModuleIntegrity();
        if (moduleIntegrity.failed > 0) {
            failures.push(`${moduleIntegrity.failed} shared module(s) failed integrity check`);
        }
        const toolRegistry = this.checkToolRegistry();
        if (toolRegistry.unapprovedDetected.length > 0) {
            failures.push(`${toolRegistry.unapprovedDetected.length} unapproved tool(s) detected`);
        }
        const result = {
            timestamp: new Date().toISOString(),
            vulnerabilities,
            lockfileIntegrity,
            moduleIntegrity,
            toolRegistry,
            passed: failures.length === 0,
            failures,
        };
        this.logAudit(result);
        return result;
    }
    /**
     * Run npm audit to check for known vulnerabilities.
     */
    checkVulnerabilities() {
        if (this.config.skipNpmAudit) {
            return { checked: false, critical: 0, high: 0, moderate: 0, low: 0, total: 0, details: 'Skipped' };
        }
        const packageJson = path.join(this.config.projectRoot, 'package.json');
        if (!fs.existsSync(packageJson)) {
            return { checked: false, critical: 0, high: 0, moderate: 0, low: 0, total: 0, details: 'No package.json' };
        }
        try {
            const result = execFileSync('npm', ['audit', '--json'], {
                cwd: this.config.projectRoot,
                encoding: 'utf-8',
                timeout: 30000,
            });
            const audit = JSON.parse(result);
            const vuln = audit.metadata?.vulnerabilities || {};
            return {
                checked: true,
                critical: vuln.critical || 0,
                high: vuln.high || 0,
                moderate: vuln.moderate || 0,
                low: vuln.low || 0,
                total: (vuln.critical || 0) + (vuln.high || 0) + (vuln.moderate || 0) + (vuln.low || 0),
            };
        }
        catch (e) {
            // npm audit returns non-zero when vulnerabilities exist
            const err = e;
            if (err.stdout) {
                try {
                    const audit = JSON.parse(err.stdout);
                    const vuln = audit.metadata?.vulnerabilities || {};
                    return {
                        checked: true,
                        critical: vuln.critical || 0,
                        high: vuln.high || 0,
                        moderate: vuln.moderate || 0,
                        low: vuln.low || 0,
                        total: (vuln.critical || 0) + (vuln.high || 0) + (vuln.moderate || 0) + (vuln.low || 0),
                    };
                }
                catch {
                    // fall through
                }
            }
            return { checked: false, critical: 0, high: 0, moderate: 0, low: 0, total: 0, details: 'npm audit failed' };
        }
    }
    /**
     * Verify lockfile hasn't been tampered with.
     */
    checkLockfile() {
        const lockfile = path.join(this.config.projectRoot, 'package-lock.json');
        if (!fs.existsSync(lockfile)) {
            return { exists: false, valid: true };
        }
        const content = fs.readFileSync(lockfile, 'utf-8');
        const hash = crypto.createHash('sha256').update(content).digest('hex');
        // Check against stored hash
        const hashFile = path.join(this.config.projectRoot, '.governance', 'lockfile-hash.txt');
        if (!fs.existsSync(hashFile)) {
            // First run - store current hash as baseline
            const dir = path.dirname(hashFile);
            if (!fs.existsSync(dir))
                fs.mkdirSync(dir, { recursive: true });
            fs.writeFileSync(hashFile, hash);
            return { exists: true, hash, valid: true };
        }
        const expectedHash = fs.readFileSync(hashFile, 'utf-8').trim();
        return {
            exists: true,
            hash,
            expectedHash,
            valid: hash === expectedHash,
        };
    }
    /**
     * Verify shared module file hashes against manifest.
     */
    checkModuleIntegrity() {
        const manifestPath = path.join(this.config.projectRoot, '.governance', 'module-manifest.json');
        if (!fs.existsSync(manifestPath)) {
            return { totalFiles: 0, verified: 0, failed: 0, mismatches: [] };
        }
        const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
        const mismatches = [];
        let verified = 0;
        for (const [relativePath, expectedHash] of Object.entries(manifest.files)) {
            const fullPath = path.join(this.config.projectRoot, relativePath);
            if (!fs.existsSync(fullPath)) {
                mismatches.push({ file: relativePath, expected: expectedHash, actual: 'MISSING' });
                continue;
            }
            const content = fs.readFileSync(fullPath, 'utf-8');
            const actualHash = crypto.createHash('sha256').update(content).digest('hex');
            if (actualHash === expectedHash) {
                verified++;
            }
            else {
                mismatches.push({ file: relativePath, expected: expectedHash, actual: actualHash });
            }
        }
        return {
            totalFiles: Object.keys(manifest.files).length,
            verified,
            failed: mismatches.length,
            mismatches,
        };
    }
    /**
     * Check if any unapproved tools/MCP servers are in use.
     */
    checkToolRegistry() {
        if (!fs.existsSync(this.config.toolRegistryPath)) {
            return { exists: false, approvedTools: 0, unapprovedDetected: [] };
        }
        const content = fs.readFileSync(this.config.toolRegistryPath, 'utf-8');
        // Parse approved tools from markdown table
        const approvedTools = [];
        const lines = content.split('\n');
        for (const line of lines) {
            const match = line.match(/^\|\s*`?([^|`]+)`?\s*\|.*\|\s*APPROVED\s*\|/i);
            if (match) {
                approvedTools.push(match[1].trim());
            }
        }
        // Check settings.json for MCP servers in use
        const unapproved = [];
        const settingsPath = path.join(this.config.projectRoot, '.claude', 'settings.json');
        if (fs.existsSync(settingsPath)) {
            try {
                const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
                const mcpServers = Object.keys(settings.mcpServers || {});
                for (const server of mcpServers) {
                    if (!approvedTools.includes(server)) {
                        unapproved.push(server);
                    }
                }
            }
            catch {
                // skip if settings can't be parsed
            }
        }
        return {
            exists: true,
            approvedTools: approvedTools.length,
            unapprovedDetected: unapproved,
        };
    }
    /**
     * Generate a module manifest from the current state of shared files.
     */
    generateManifest(agentName) {
        const files = {};
        const walkDir = (dir) => {
            if (!fs.existsSync(dir))
                return;
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            for (const entry of entries) {
                const full = path.join(dir, entry.name);
                if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {
                    walkDir(full);
                }
                else if (entry.isFile() && /\.(js|ts|sh|json|md)$/.test(entry.name)) {
                    const content = fs.readFileSync(full, 'utf-8');
                    const hash = crypto.createHash('sha256').update(content).digest('hex');
                    const relative = path.relative(this.config.projectRoot, full);
                    files[relative] = hash;
                }
            }
        };
        if (fs.existsSync(this.config.sharedDir)) {
            walkDir(this.config.sharedDir);
        }
        const manifest = {
            generatedAt: new Date().toISOString(),
            generatedBy: agentName,
            projectRoot: this.config.projectRoot,
            files,
        };
        // Save manifest
        const manifestPath = path.join(this.config.projectRoot, '.governance', 'module-manifest.json');
        const dir = path.dirname(manifestPath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
        return manifest;
    }
    /**
     * Update the lockfile hash baseline (after legitimate dependency changes).
     */
    updateLockfileBaseline() {
        const lockfile = path.join(this.config.projectRoot, 'package-lock.json');
        if (!fs.existsSync(lockfile))
            return null;
        const content = fs.readFileSync(lockfile, 'utf-8');
        const hash = crypto.createHash('sha256').update(content).digest('hex');
        const hashFile = path.join(this.config.projectRoot, '.governance', 'lockfile-hash.txt');
        const dir = path.dirname(hashFile);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        fs.writeFileSync(hashFile, hash);
        return hash;
    }
    // --- Private helpers ---
    logAudit(result) {
        const logDir = path.dirname(this.config.auditLogPath);
        if (!fs.existsSync(logDir)) {
            fs.mkdirSync(logDir, { recursive: true });
        }
        const entry = {
            timestamp: result.timestamp,
            event: 'supply-chain:audit',
            passed: result.passed,
            failures: result.failures,
            vulnTotal: result.vulnerabilities.total,
            vulnCritical: result.vulnerabilities.critical,
            modulesVerified: result.moduleIntegrity.verified,
            modulesFailed: result.moduleIntegrity.failed,
        };
        fs.appendFileSync(this.config.auditLogPath, JSON.stringify(entry) + '\n');
    }
}
//# sourceMappingURL=supply-chain.js.map