// engine/supply-chain/sbom-checker.ts — SPEC-046
// Detects SBOM presence, format, and validates CISA Minimum Elements.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// SBOM file patterns
const SBOM_GLOBS = [
    '**/*.spdx',
    '**/*.sbom.json',
    '**/bom.xml',
    '**/sbom.spdx.json',
    '**/sbom.json',
    '**/bom.json',
];
// CI-based SBOM generators
const SBOM_CI_PATTERNS = [
    'trivy sbom',
    'syft',
    'cyclonedx',
    '@cyclonedx',
    'cyclonedx-npm',
    'cyclonedx-bom',
    'cyclonedx-gomod',
    'cyclonedx-maven',
    'cargo-sbom',
];
// CISA Minimum Elements (2025) that must be present in a valid SBOM
const CISA_MINIMUM_FIELDS = [
    'supplier',
    'author',
    'timestamp',
    'name',
    'version',
    'serialNumber',
    'dependencies',
];
const SBOM_REFS = [
    'https://www.cisa.gov/resources-tools/resources/software-bill-materials-sbom',
    'https://cyclonedx.org/specification/overview/',
    'https://spdx.dev/',
];
/** Find all SBOM files in the project. */
async function findSbomFiles(projectPath) {
    try {
        const files = await glob(SBOM_GLOBS, {
            cwd: projectPath,
            nodir: true,
            maxDepth: 5,
            ignore: ['node_modules/**', 'vendor/**', '.git/**'],
        });
        return files.map((f) => join(projectPath, f));
    }
    catch {
        return [];
    }
}
/** Detect SBOM format from file content. */
function detectSbomFormat(content, fileName) {
    // SPDX detection takes priority (check before CycloneDX to avoid sbom.spdx false-positive)
    if (content.includes('SPDXVersion') || content.includes('spdxId') || fileName.includes('.spdx')) {
        return 'SPDX-2.3';
    }
    // CycloneDX detection — match file basename only (avoid sbom.spdx matching /bom./)
    const baseName = fileName.split('/').pop() ?? fileName;
    if (content.includes('cyclonedx') ||
        content.includes('CycloneDX') ||
        baseName === 'bom.xml' ||
        baseName === 'bom.json') {
        return 'CycloneDX-1.6';
    }
    return 'other';
}
/** Validate a CycloneDX/SPDX SBOM against CISA Minimum Elements. */
function validateSbomContent(content) {
    const lower = content.toLowerCase();
    // Check that all 7 CISA minimum fields are present
    return CISA_MINIMUM_FIELDS.every((field) => lower.includes(field.toLowerCase()));
}
/** Check if CI workflows contain SBOM generation commands. */
async function hasSbomInCi(projectPath) {
    try {
        const workflowFiles = await glob(['.github/workflows/*.yml', '.github/workflows/*.yaml'], {
            cwd: projectPath,
            nodir: true,
            absolute: false,
        });
        for (const f of workflowFiles) {
            try {
                const content = await readFile(join(projectPath, f), 'utf-8');
                if (SBOM_CI_PATTERNS.some((p) => content.toLowerCase().includes(p.toLowerCase()))) {
                    return true;
                }
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    catch {
        // No .github/workflows directory
    }
    return false;
}
/**
 * Detect SBOM presence and validate its content.
 * Returns SbomCheckResult with format, validity, and CI integration status.
 */
export async function checkSbom(projectPath) {
    const sbomFiles = await findSbomFiles(projectPath);
    const sbomInCi = await hasSbomInCi(projectPath);
    if (sbomFiles.length === 0 && !sbomInCi) {
        return {
            sbomPresent: false,
            sbomValid: false,
            sbomFormat: null,
            sbomInCi: false,
        };
    }
    // If no file on disk but CI generates it — mark as present but not locally validatable
    if (sbomFiles.length === 0) {
        return {
            sbomPresent: true,
            sbomValid: false, // Cannot validate without the file
            sbomFormat: null,
            sbomInCi: true,
        };
    }
    // Validate the first SBOM file found
    const firstFile = sbomFiles[0];
    if (!firstFile) {
        return { sbomPresent: false, sbomValid: false, sbomFormat: null, sbomInCi };
    }
    try {
        const content = await readFile(firstFile, 'utf-8');
        const format = detectSbomFormat(content, firstFile);
        const valid = validateSbomContent(content);
        return { sbomPresent: true, sbomValid: valid, sbomFormat: format, sbomInCi };
    }
    catch {
        return { sbomPresent: true, sbomValid: false, sbomFormat: null, sbomInCi };
    }
}
/**
 * Build SBOM gaps from check results.
 */
export function buildSbomGaps(result, isGovernmentOrEnterprise) {
    const gaps = [];
    if (!result.sbomPresent) {
        const severity = isGovernmentOrEnterprise ? 'critical' : 'warning';
        gaps.push({
            category: 'sbom',
            severity,
            message: 'No SBOM detected — project does not generate a Software Bill of Materials',
            fix: 'Add SBOM generation to CI using syft, cyclonedx-npm, or equivalents; publish alongside artifacts',
            references: SBOM_REFS,
        });
        return gaps;
    }
    // result.sbomPresent is always true here (early return above handles the false case)
    if (!result.sbomValid) {
        gaps.push({
            category: 'sbom',
            severity: 'warning',
            message: 'SBOM found but missing CISA Minimum Elements (2025): supplier, author, timestamp, name, version, serialNumber, dependencies',
            fix: 'Regenerate SBOM with a tool that includes all 7 CISA Minimum Elements (CycloneDX v1.6+ recommended)',
            references: SBOM_REFS,
        });
    }
    return gaps;
}
//# sourceMappingURL=sbom-checker.js.map