// engine/supply-chain/slsa-checker.ts — SPEC-046
// Detects SLSA provenance level (L0..L3) from CI workflows and project files.
// TD-P15-01: SLSA v1.1 format detection (slsa.dev/provenance/v1 + legacy v0.2)
// TD-P15-02: npm provenance via actions/attest-build-provenance (native SLSA L2)
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// SLSA GitHub Generator workflow action
const SLSA_GENERATOR_ACTION = 'slsa-framework/slsa-github-generator';
// Actions/tools that provide SLSA L2 provenance
const SLSA_L2_PATTERNS = ['actions/attest-build-provenance', 'sigstore/cosign-installer', 'cosign'];
// CI files that indicate at least L1 (scripted build)
const CI_WORKFLOW_GLOBS = ['.github/workflows/*.yml', '.github/workflows/*.yaml'];
// Publish-indicating script patterns
const PUBLISH_PATTERNS = ['npm publish', 'twine upload', 'cargo publish', 'mvn deploy'];
// SLSA references
const SLSA_REFS = [
    'https://slsa.dev/spec/v1.0/',
    'https://github.com/slsa-framework/slsa-github-generator',
    'https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds',
];
// SLSA v1.1 provenance format (April 2025) — replaces v0.2
// Both formats are valid; v1 is preferred for new setups.
const SLSA_PROVENANCE_V1_FORMAT = 'slsa.dev/provenance/v1';
const SLSA_PROVENANCE_V02_FORMAT = 'slsa.dev/provenance/v0.2';
/** Read a file and return its content, or empty string on error. */
async function readFileSafe(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
    }
    catch {
        return '';
    }
}
/** Find all GitHub Actions workflow files. */
async function findWorkflowFiles(projectPath) {
    try {
        const files = await glob(CI_WORKFLOW_GLOBS, {
            cwd: projectPath,
            nodir: true,
            absolute: false,
        });
        return files.map((f) => join(projectPath, f));
    }
    catch {
        return [];
    }
}
/** Check if any workflow file contains a SLSA L3 generator action. */
async function hasSlsaGeneratorWorkflow(workflowFiles) {
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (content.includes(SLSA_GENERATOR_ACTION)) {
            return true;
        }
    }
    return false;
}
/** Check if any workflow file contains SLSA L2 patterns (cosign/attestation). */
async function hasSlsaL2Workflow(workflowFiles) {
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (SLSA_L2_PATTERNS.some((p) => content.includes(p))) {
            return true;
        }
    }
    return false;
}
/** Check if the project has evidence of publishing artifacts. */
async function hasPublishEvidence(projectPath, workflowFiles) {
    // Check package.json scripts
    try {
        const pkg = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const scripts = JSON.parse(pkg).scripts ?? {};
        if (Object.values(scripts).some((s) => s.includes('npm publish'))) {
            return true;
        }
    }
    catch {
        // No package.json
    }
    // Check CI workflows for publish patterns
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (PUBLISH_PATTERNS.some((p) => content.includes(p))) {
            return true;
        }
    }
    return false;
}
/** Check if any workflow file has the OIDC id-token permission for signing. */
async function hasOidcPermission(workflowFiles) {
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (content.includes('id-token: write')) {
            return true;
        }
    }
    return false;
}
/** Check if signing artifacts (.sigstore) are present in CI. */
async function hasSigningArtifacts(workflowFiles) {
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (content.includes('.sigstore') || content.includes('sigstore')) {
            return true;
        }
    }
    return false;
}
/**
 * Detect the SLSA level of the project based on CI configuration.
 * Returns the SLSA level and whether CI workflows exist.
 */
export async function detectSlsaLevel(projectPath) {
    const workflowFiles = await findWorkflowFiles(projectPath);
    const hasCi = workflowFiles.length > 0;
    if (!hasCi) {
        return { level: 'L0', hasCi: false, isPublishing: false, hasOidc: false };
    }
    const [hasL3, hasL2, isPublishing, hasOidc] = await Promise.all([
        hasSlsaGeneratorWorkflow(workflowFiles),
        hasSlsaL2Workflow(workflowFiles),
        hasPublishEvidence(projectPath, workflowFiles),
        hasOidcPermission(workflowFiles),
    ]);
    // hasCi is always true here (early return above handles the false case)
    let level;
    if (hasL3) {
        level = 'L3';
    }
    else if (hasL2 || (hasOidc && (await hasSigningArtifacts(workflowFiles)))) {
        level = 'L2';
    }
    else {
        level = 'L1';
    }
    return { level, hasCi, isPublishing, hasOidc };
}
/**
 * Detect which SLSA provenance format is declared in the project's attestation files.
 * SLSA v1.1 (April 2025) uses `slsa.dev/provenance/v1`.
 * Legacy projects may still use `slsa.dev/provenance/v0.2`.
 * Both are valid; v1 is preferred going forward.
 */
export async function detectSlsaProvenanceFormat(projectPath) {
    const workflowFiles = await findWorkflowFiles(projectPath);
    let hasV1 = false;
    let hasV02 = false;
    for (const file of workflowFiles) {
        const content = await readFileSafe(file);
        if (content.includes(SLSA_PROVENANCE_V1_FORMAT)) {
            hasV1 = true;
        }
        if (content.includes(SLSA_PROVENANCE_V02_FORMAT)) {
            hasV02 = true;
        }
    }
    if (hasV1 && hasV02) {
        return 'both';
    }
    if (hasV1) {
        return 'v1';
    }
    if (hasV02) {
        return 'v0.2';
    }
    return 'none';
}
/**
 * Detect npm native provenance via `npm publish --provenance` + `actions/attest-build-provenance`.
 * When both are present, the project achieves SLSA L2 without external tooling.
 * Reference: https://docs.npmjs.com/generating-provenance-statements
 */
export async function detectNpmProvenance(projectPath, workflowFiles) {
    const files = workflowFiles ?? (await findWorkflowFiles(projectPath));
    let hasNpmProvenanceScript = false;
    let hasAttestAction = false;
    // Check package.json scripts for `npm publish --provenance`
    try {
        const pkg = await readFile(join(projectPath, 'package.json'), 'utf-8');
        const scripts = JSON.parse(pkg).scripts ?? {};
        hasNpmProvenanceScript = Object.values(scripts).some((s) => s.includes('npm publish') && s.includes('--provenance'));
    }
    catch {
        // No package.json — not an npm project
    }
    // Check if any workflow uses actions/attest-build-provenance
    for (const file of files) {
        const content = await readFileSafe(file);
        if (content.includes('actions/attest-build-provenance')) {
            hasAttestAction = true;
            break;
        }
    }
    return {
        hasNpmProvenanceScript,
        hasAttestAction,
        isNativeSlsaL2: hasNpmProvenanceScript && hasAttestAction,
    };
}
/**
 * Build SLSA gaps from detection results.
 */
export function buildSlsaGaps(level, isPublishing, hasOidc) {
    const gaps = [];
    if (level === 'L0' && isPublishing) {
        gaps.push({
            category: 'slsa',
            severity: 'critical',
            message: 'Project publishes artifacts but has no SLSA provenance (L0) — cannot verify build integrity',
            fix: 'Add `actions/attest-build-provenance` to your release workflow to achieve SLSA L2',
            references: SLSA_REFS,
        });
    }
    else if (level === 'L0') {
        gaps.push({
            category: 'slsa',
            severity: 'warning',
            message: 'No CI detected — build is not scripted (SLSA L0)',
            fix: 'Add GitHub Actions or equivalent CI to script the build (achieves SLSA L1)',
            references: [SLSA_REFS[0] ?? ''],
        });
    }
    else if (level === 'L1' && isPublishing) {
        gaps.push({
            category: 'slsa',
            severity: 'warning',
            message: 'Project publishes artifacts but only achieves SLSA L1 — no signed provenance',
            fix: 'Add `actions/attest-build-provenance` with `permissions: id-token: write` to achieve SLSA L2',
            references: SLSA_REFS,
        });
    }
    if ((level === 'L1' || level === 'L2') && !hasOidc && isPublishing) {
        gaps.push({
            category: 'slsa',
            severity: 'warning',
            message: 'Release workflow missing `permissions: id-token: write` — OIDC signing not enabled',
            fix: 'Add `permissions: id-token: write` to the job in your release workflow',
            references: [SLSA_REFS[2] ?? ''],
        });
    }
    return gaps;
}
//# sourceMappingURL=slsa-checker.js.map