// engine/detectors/supply-chain-detector.ts — Supply chain security detection (SPEC-026b)
// Detects SBOM generation, container image signing, vulnerability scanning.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// === Detection signals ===
// SBOM generation tools / pipeline steps
const SBOM_TOOL_SIGNALS = ['syft', 'sbom-action', 'attest-build-provenance', 'cyclonedx', 'spdx'];
const SBOM_CONFIG_FILES = ['.syft.yaml', '.syft.yml', 'syft.yaml', 'cyclonedx.json'];
const SBOM_GLOB_PATTERNS = ['**/.syft.yaml', '**/sbom.*.json', '**/sbom.spdx*'];
// Image signing tools
const SIGNING_SIGNALS = ['cosign', 'sigstore', 'notation', 'docker trust'];
const SIGNING_CONFIG_FILES = ['.cosign.yaml', 'cosign.key.pub'];
// Vulnerability scanning tools
const VULN_SCAN_SIGNALS = ['grype', 'trivy', 'snyk', 'anchore'];
const VULN_CONFIG_FILES = ['.grype.yaml', '.grype.yml', 'trivy.yaml', 'trivy.yml', '.snyk'];
// Public image distribution signals
const PUBLIC_REGISTRY_SIGNALS = [
    'docker.io/',
    'hub.docker.com',
    'ghcr.io',
    '*.dkr.ecr.*.amazonaws.com',
];
const PUBLIC_REGISTRY_GLOB_PATTERNS = [
    '**/Dockerfile',
    '.github/workflows/*.yml',
    '.github/workflows/*.yaml',
];
// === Public API ===
/**
 * Detects supply chain security tools and configuration.
 * Checks for SBOM generation, image signing, and vulnerability scanning.
 */
export async function detectSupplyChain(projectPath) {
    // Check CI pipeline content for SBOM / signing / scanning tools
    const ciContent = await readCiContent(projectPath);
    const packageContent = await readPackageContent(projectPath);
    const sbomTools = detectSbomTools(ciContent, packageContent, projectPath);
    const hasSbom = sbomTools.length > 0 || (await hasAnySbomConfigFiles(projectPath));
    const hasImageSigning = SIGNING_SIGNALS.some((s) => ciContent.includes(s)) ||
        (await hasAnyFiles(projectPath, SIGNING_CONFIG_FILES));
    const scanningTools = detectVulnScanTools(ciContent, packageContent);
    const hasVulnScanning = scanningTools.length > 0 || (await hasAnyFiles(projectPath, VULN_CONFIG_FILES));
    const distributesPublicImages = await detectPublicImageDistribution(projectPath, ciContent);
    const sbomGapSeverity = computeSbomGapSeverity(hasSbom, distributesPublicImages);
    return {
        hasSbom,
        sbomTools,
        hasImageSigning,
        hasVulnScanning,
        scanningTools,
        distributesPublicImages,
        sbomGapSeverity,
    };
}
// === Internal helpers ===
async function readCiContent(projectPath) {
    const contents = [];
    const patterns = [
        '.github/workflows/*.yml',
        '.github/workflows/*.yaml',
        '.gitlab-ci.yml',
        '.gitlab-ci.yaml',
        '.circleci/config.yml',
        'azure-pipelines.yml',
        'Jenkinsfile',
    ];
    for (const pattern of patterns) {
        try {
            const matches = pattern.includes('*')
                ? await glob(pattern, { cwd: projectPath, nodir: true })
                : [pattern];
            for (const match of matches) {
                try {
                    const content = await readFile(join(projectPath, match), 'utf-8');
                    contents.push(content);
                }
                catch {
                    // ignore
                }
            }
        }
        catch {
            // ignore glob errors
        }
    }
    return contents.join('\n');
}
async function readPackageContent(projectPath) {
    try {
        return await readFile(join(projectPath, 'package.json'), 'utf-8');
    }
    catch {
        return '';
    }
}
function detectSbomTools(ciContent, packageContent, _projectPath) {
    const found = [];
    const combined = `${ciContent}\n${packageContent}`.toLowerCase();
    for (const tool of SBOM_TOOL_SIGNALS) {
        if (combined.includes(tool.toLowerCase())) {
            found.push(tool);
        }
    }
    return [...new Set(found)];
}
async function hasAnySbomConfigFiles(projectPath) {
    // Check static paths
    if (await hasAnyFiles(projectPath, SBOM_CONFIG_FILES)) {
        return true;
    }
    // Check glob patterns
    for (const pattern of SBOM_GLOB_PATTERNS) {
        try {
            const matches = await glob(pattern, { cwd: projectPath, nodir: true });
            if (matches.length > 0) {
                return true;
            }
        }
        catch {
            // ignore
        }
    }
    return false;
}
function detectVulnScanTools(ciContent, packageContent) {
    const found = [];
    const combined = `${ciContent}\n${packageContent}`.toLowerCase();
    for (const tool of VULN_SCAN_SIGNALS) {
        if (combined.includes(tool.toLowerCase())) {
            found.push(tool);
        }
    }
    return [...new Set(found)];
}
async function detectPublicImageDistribution(projectPath, ciContent) {
    // Check CI for public registry pushes
    if (PUBLIC_REGISTRY_SIGNALS.some((s) => ciContent.toLowerCase().includes(s.toLowerCase()))) {
        return true;
    }
    // Check Dockerfile for FROM public base images pushed to public registries
    for (const pattern of PUBLIC_REGISTRY_GLOB_PATTERNS) {
        try {
            const matches = await glob(pattern, { cwd: projectPath, nodir: true });
            for (const match of matches) {
                try {
                    const content = await readFile(join(projectPath, match), 'utf-8');
                    // Look for docker push to public registry
                    if (content.includes('docker push') &&
                        (content.includes('docker.io') ||
                            content.includes('ghcr.io') ||
                            content.includes('hub.docker.com'))) {
                        return true;
                    }
                }
                catch {
                    // ignore
                }
            }
        }
        catch {
            // ignore glob errors
        }
    }
    return false;
}
function computeSbomGapSeverity(hasSbom, distributesPublicImages) {
    if (hasSbom) {
        return 'none';
    }
    if (distributesPublicImages) {
        return 'critical';
    }
    return 'recommended';
}
async function hasAnyFiles(projectPath, paths) {
    for (const p of paths) {
        try {
            await readFile(join(projectPath, p), 'utf-8');
            return true;
        }
        catch {
            // not found
        }
    }
    return false;
}
//# sourceMappingURL=supply-chain-detector.js.map