"use strict";
/**
 * CLI Terminal Formatter
 * Formats scan results with ANSI colors for terminal output
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatTerminalOutput = formatTerminalOutput;
exports.formatCompactSummary = formatCompactSummary;
exports.getNumberedFindings = getNumberedFindings;
exports.formatFindingDetail = formatFindingDetail;
exports.formatSimpleList = formatSimpleList;
exports.formatJSON = formatJSON;
exports.formatSARIF = formatSARIF;
const grouping_1 = require("./grouping");
const hash_1 = require("../suppression/hash");
/**
 * ANSI color codes
 */
const colors = {
    reset: '\x1b[0m',
    bold: '\x1b[1m',
    dim: '\x1b[2m',
    underline: '\x1b[4m',
    // Foreground colors
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    magenta: '\x1b[35m',
    cyan: '\x1b[36m',
    white: '\x1b[37m',
    gray: '\x1b[90m',
    // Background colors
    bgRed: '\x1b[41m',
    bgYellow: '\x1b[43m',
    bgBlue: '\x1b[44m',
};
/**
 * Severity colors and symbols
 */
const SEVERITY_STYLE = {
    critical: { color: colors.bgRed + colors.white, symbol: '●', label: 'CRITICAL' },
    high: { color: colors.red, symbol: '●', label: 'HIGH' },
    medium: { color: colors.yellow, symbol: '●', label: 'MEDIUM' },
    low: { color: colors.blue, symbol: '○', label: 'LOW' },
    info: { color: colors.gray, symbol: '○', label: 'INFO' },
};
/**
 * Format colored text
 */
function c(color, text) {
    return `${color}${text}${colors.reset}`;
}
/**
 * Format severity badge
 */
function severityBadge(severity) {
    const style = SEVERITY_STYLE[severity];
    return c(style.color, `${style.symbol} ${style.label}`);
}
/**
 * Format a single finding for terminal
 * Default: Actionable output with impact, code, and fix steps
 * Compact: Severity + title + location only
 * Verbose: All of the above plus references and validation notes
 */
function formatFinding(finding, options = {}) {
    const { indent = '  ', compact = false, verbose = false } = options;
    const badge = severityBadge(finding.severity);
    const location = c(colors.cyan, `${finding.filePath}:${finding.lineNumber}`);
    const hash = (0, hash_1.computeFindingHash)(finding);
    // Compact mode: just severity, title, and location
    if (compact) {
        return `${indent}${badge} ${c(colors.bold, finding.title)}  ${location}\n`;
    }
    // Default actionable output
    let output = `${indent}${badge} ${c(colors.bold, finding.title)}\n`;
    output += `${indent}   ${location}\n`;
    output += '\n';
    // Impact (why this matters) - shown by default
    if (finding.impact) {
        output += `${indent}   ${c(colors.yellow + colors.bold, 'Impact:')} ${finding.impact}\n`;
        output += '\n';
    }
    // Code snippet
    if (finding.lineContent && finding.lineContent.trim()) {
        output += `${indent}   ${c(colors.dim, 'Code:')} ${c(colors.white, finding.lineContent.trim().substring(0, 80))}${finding.lineContent.trim().length > 80 ? '...' : ''}\n`;
        output += '\n';
    }
    // Fix steps - shown by default (numbered list)
    if (finding.fixSteps && finding.fixSteps.length > 0) {
        output += `${indent}   ${c(colors.green + colors.bold, 'Fix:')}\n`;
        finding.fixSteps.forEach((step, i) => {
            output += `${indent}   ${c(colors.green, `${i + 1}. ${step}`)}\n`;
        });
        output += '\n';
    }
    else if (finding.suggestedFix) {
        // Fallback to legacy suggestedFix field
        output += `${indent}   ${c(colors.green, '💡 ' + finding.suggestedFix)}\n`;
        output += '\n';
    }
    // Verbose mode: show additional details
    if (verbose) {
        // Description
        output += `${indent}   ${c(colors.dim, finding.description)}\n`;
        // References (OWASP/CWE links)
        if (finding.references && finding.references.length > 0) {
            output += `${indent}   ${c(colors.blue, 'References:')}\n`;
            finding.references.forEach(ref => {
                output += `${indent}   ${c(colors.blue, `  • ${ref}`)}\n`;
            });
        }
        // Validation notes (if AI validated)
        if (finding.validationNotes) {
            output += `${indent}   ${c(colors.dim, `[AI] ${finding.validationNotes}`)}\n`;
        }
        // AI enhanced indicator
        if (finding.aiEnhanced) {
            output += `${indent}   ${c(colors.magenta, '✨ AI-enhanced fix suggestion')}\n`;
        }
    }
    // Suppress command - always shown
    output += `${indent}   ${c(colors.dim, `Suppress: oculum ignore ${hash} --file "${finding.filePath}:${finding.lineNumber}" --reason "..."`)}\n`;
    return output;
}
/**
 * Format a group of findings
 */
function formatGroup(group, options = {}) {
    const { maxFindings = 10, compact = false, verbose = false } = options;
    const { theme, themeName, findings, severityCounts } = group;
    const config = grouping_1.THEME_CONFIG[theme];
    // Count summary
    const counts = [];
    if (severityCounts.critical > 0)
        counts.push(c(colors.red, `${severityCounts.critical} critical`));
    if (severityCounts.high > 0)
        counts.push(c(colors.red, `${severityCounts.high} high`));
    if (severityCounts.medium > 0)
        counts.push(c(colors.yellow, `${severityCounts.medium} medium`));
    if (severityCounts.low > 0)
        counts.push(c(colors.blue, `${severityCounts.low} low`));
    if (severityCounts.info > 0)
        counts.push(c(colors.gray, `${severityCounts.info} info`));
    let output = `\n${c(colors.bold, `${config.icon} ${themeName}`)} (${counts.join(', ')})\n`;
    output += c(colors.dim, '─'.repeat(60)) + '\n';
    // Show findings
    const shown = findings.slice(0, maxFindings);
    for (const finding of shown) {
        output += formatFinding(finding, { compact, verbose }) + '\n';
    }
    // Truncation notice
    if (findings.length > maxFindings) {
        output += c(colors.dim, `  ... and ${findings.length - maxFindings} more\n`);
    }
    return output;
}
/**
 * Format baseline diff summary
 */
function formatDiffSummary(baselineDiff) {
    let output = '';
    output += c(colors.bold, 'Baseline Comparison') + '\n';
    output += c(colors.dim, '─'.repeat(40)) + '\n';
    output += `  🆕 ${c(colors.yellow, `${baselineDiff.newCount} new`)} findings\n`;
    output += `  ✅ ${c(colors.green, `${baselineDiff.fixedCount} fixed`)} since baseline\n`;
    output += `  📋 ${c(colors.dim, `${baselineDiff.existingCount} existing`)} (in baseline)\n`;
    output += '\n';
    // Format baseline date
    const baselineDate = new Date(baselineDiff.baselineCreatedAt);
    const dateStr = baselineDate.toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
    });
    const commitStr = baselineDiff.baselineCommit ? ` (${baselineDiff.baselineCommit})` : '';
    output += c(colors.dim, `Baseline from ${dateStr}${commitStr}`) + '\n\n';
    return output;
}
/**
 * Format full scan result for terminal
 */
function formatTerminalOutput(result, options = {}) {
    const { maxFindingsPerGroup = 10, showAllFindings = false, compact = false, verbose = false, } = options;
    const { vulnerabilities, severityCounts, hasBlockingIssues, filesScanned, scanDuration, baselineDiff } = result;
    let output = '\n';
    // Header
    output += c(colors.bold, '═'.repeat(60)) + '\n';
    output += c(colors.bold, '  OCULUM SECURITY SCAN RESULTS') + '\n';
    output += c(colors.bold, '═'.repeat(60)) + '\n\n';
    // Baseline diff summary (if present)
    if (baselineDiff) {
        output += formatDiffSummary(baselineDiff);
    }
    // Status
    if (hasBlockingIssues) {
        const blocking = severityCounts.critical + severityCounts.high;
        output += c(colors.bgRed + colors.white + colors.bold, ` 🚨 ${blocking} BLOCKING ISSUES FOUND `) + '\n\n';
    }
    else if (vulnerabilities.length > 0) {
        output += c(colors.yellow, `⚠️  ${vulnerabilities.length} issues found (no blocking issues)`) + '\n\n';
    }
    else {
        output += c(colors.green, '✅ No security issues found!') + '\n\n';
        output += c(colors.dim, `Scanned ${filesScanned} files in ${(scanDuration / 1000).toFixed(1)}s`) + '\n';
        return output;
    }
    // Summary counts
    output += c(colors.bold, 'Summary:') + '\n';
    if (severityCounts.critical > 0)
        output += `  ${severityBadge('critical')}  ${severityCounts.critical}\n`;
    if (severityCounts.high > 0)
        output += `  ${severityBadge('high')}      ${severityCounts.high}\n`;
    if (severityCounts.medium > 0)
        output += `  ${severityBadge('medium')}    ${severityCounts.medium}\n`;
    if (severityCounts.low > 0)
        output += `  ${severityBadge('low')}       ${severityCounts.low}\n`;
    if (severityCounts.info > 0)
        output += `  ${severityBadge('info')}      ${severityCounts.info}\n`;
    output += '\n';
    // Blocking issues first
    const blockingIssues = (0, grouping_1.getBlockingIssues)(vulnerabilities);
    if (blockingIssues.length > 0) {
        output += c(colors.bgRed + colors.white + colors.bold, ' BLOCKING ISSUES ') + '\n';
        output += c(colors.red, 'These must be fixed before merging:') + '\n\n';
        for (const finding of blockingIssues.slice(0, 10)) {
            output += formatFinding(finding, { compact, verbose });
            output += '\n';
        }
        if (blockingIssues.length > 10) {
            output += c(colors.dim, `  ... and ${blockingIssues.length - 10} more blocking issues\n`);
        }
        output += '\n';
    }
    // Grouped findings
    const grouped = (0, grouping_1.groupByTheme)(vulnerabilities);
    output += c(colors.bold, '─'.repeat(60)) + '\n';
    output += c(colors.bold, 'ALL FINDINGS BY CATEGORY') + '\n';
    for (const group of grouped) {
        // Skip if only showing non-blocking and all are blocking
        if (!showAllFindings) {
            const nonBlocking = group.findings.filter(f => f.severity !== 'critical' && f.severity !== 'high');
            if (nonBlocking.length === 0 && blockingIssues.length > 0)
                continue;
        }
        output += formatGroup(group, { maxFindings: maxFindingsPerGroup, compact, verbose });
    }
    // Suppressed findings section (if any)
    if (result.suppressedVulnerabilities && result.suppressedVulnerabilities.length > 0) {
        output += '\n' + c(colors.dim, '─'.repeat(60)) + '\n';
        output += c(colors.dim + colors.bold, 'SUPPRESSED FINDINGS') + '\n';
        output += c(colors.dim, `${result.suppressedVulnerabilities.length} findings suppressed`) + '\n\n';
        for (const suppressed of result.suppressedVulnerabilities.slice(0, 5)) {
            const typeLabel = suppressed.suppressionType === 'inline' ? 'inline'
                : suppressed.suppressionType === 'config-finding' ? 'config'
                    : 'rule';
            output += c(colors.dim, `  ${suppressed.hash.slice(0, 8)} ${suppressed.filePath}:${suppressed.lineNumber}`) + '\n';
            output += c(colors.dim, `    ${suppressed.title}`) + '\n';
            output += c(colors.dim, `    [${typeLabel}] ${suppressed.suppressionReason}`) + '\n';
            if (suppressed.expires) {
                output += c(colors.dim, `    Expires: ${suppressed.expires}`) + '\n';
            }
            output += '\n';
        }
        if (result.suppressedVulnerabilities.length > 5) {
            output += c(colors.dim, `  ... and ${result.suppressedVulnerabilities.length - 5} more suppressed\n`);
        }
    }
    // Suppression stats (if any)
    if (result.suppressionStats && (result.suppressionStats.inlineSuppressed > 0 ||
        result.suppressionStats.configFindingSuppressed > 0 ||
        result.suppressionStats.configRuleSuppressed > 0)) {
        const stats = result.suppressionStats;
        const parts = [];
        if (stats.inlineSuppressed > 0)
            parts.push(`${stats.inlineSuppressed} inline`);
        if (stats.configFindingSuppressed > 0)
            parts.push(`${stats.configFindingSuppressed} config`);
        if (stats.configRuleSuppressed > 0)
            parts.push(`${stats.configRuleSuppressed} rule`);
        if (stats.expired > 0)
            parts.push(`${stats.expired} expired`);
        if (!result.suppressedVulnerabilities) {
            output += '\n' + c(colors.dim, `Suppressed: ${parts.join(', ')}`) + '\n';
        }
    }
    // Footer
    output += '\n' + c(colors.dim, '─'.repeat(60)) + '\n';
    output += c(colors.dim, `Scanned ${filesScanned} files in ${(scanDuration / 1000).toFixed(1)}s`) + '\n';
    return output;
}
/**
 * Format compact summary grouped by severity
 * Output format:
 *   HIGH (2)
 *      1. SQL injection in src/api/users.ts:42
 *      2. Missing auth in src/api/admin.ts:15
 */
function formatCompactSummary(vulnerabilities, options = {}) {
    const { showNumbers = true, maxPerSeverity = 5, showHint = true, noColor = false, } = options;
    if (vulnerabilities.length === 0) {
        return noColor
            ? 'No security issues found.'
            : c(colors.green, '✓ No security issues found.');
    }
    // Group by severity
    const bySeverity = {
        critical: [],
        high: [],
        medium: [],
        low: [],
        info: [],
    };
    for (const v of vulnerabilities) {
        bySeverity[v.severity].push(v);
    }
    // Build output
    let output = '';
    let globalIndex = 1;
    const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
    const severityColors = {
        critical: colors.bgRed + colors.white,
        high: colors.red,
        medium: colors.yellow,
        low: colors.blue,
        info: colors.gray,
    };
    for (const severity of severityOrder) {
        const findings = bySeverity[severity];
        if (findings.length === 0)
            continue;
        // Severity header
        const label = severity.toUpperCase();
        const header = noColor
            ? `${label} (${findings.length})`
            : c(severityColors[severity] + colors.bold, `${label} (${findings.length})`);
        output += `\n  ${header}\n`;
        // Show findings
        const shown = findings.slice(0, maxPerSeverity);
        for (const finding of shown) {
            const num = showNumbers ? `${globalIndex}. ` : '';
            const location = noColor
                ? `${finding.filePath}:${finding.lineNumber}`
                : c(colors.cyan, `${finding.filePath}:${finding.lineNumber}`);
            output += noColor
                ? `     ${num}${finding.title} in ${location}\n`
                : `     ${c(colors.dim, num)}${finding.title} ${c(colors.dim, 'in')} ${location}\n`;
            globalIndex++;
        }
        // Show truncation notice
        if (findings.length > maxPerSeverity) {
            const more = findings.length - maxPerSeverity;
            const truncated = noColor
                ? `     ... and ${more} more\n`
                : c(colors.dim, `     ... and ${more} more\n`);
            output += truncated;
            globalIndex += more; // Increment for hidden findings
        }
    }
    // Hint at bottom
    if (showHint && vulnerabilities.length > 0) {
        output += '\n';
        output += noColor
            ? "Run 'oculum show 1' for details · 'oculum fix' for suggestions\n"
            : c(colors.dim, "Run 'oculum show 1' for details · 'oculum fix' for suggestions\n");
    }
    return output;
}
/**
 * Format a numbered finding list for the show command
 * Returns findings with their numbers for reference
 */
function getNumberedFindings(vulnerabilities) {
    return vulnerabilities.map((finding, index) => ({
        number: index + 1,
        finding,
    }));
}
/**
 * Format a single finding detail view for the show command
 */
function formatFindingDetail(finding, number, options = {}) {
    const { verbose = false, noColor = false } = options;
    let output = '';
    // Header
    const badge = noColor
        ? `[${finding.severity.toUpperCase()}]`
        : severityBadge(finding.severity);
    const title = noColor ? finding.title : c(colors.bold, finding.title);
    output += `\n#${number} ${badge} ${title}\n`;
    // Location
    const location = noColor
        ? finding.filePath + ':' + finding.lineNumber
        : c(colors.cyan, `${finding.filePath}:${finding.lineNumber}`);
    output += `   ${location}\n`;
    output += '\n';
    // Impact
    if (finding.impact) {
        const impactLabel = noColor ? 'Impact:' : c(colors.yellow + colors.bold, 'Impact:');
        output += `   ${impactLabel} ${finding.impact}\n`;
        output += '\n';
    }
    // Code snippet
    if (finding.lineContent && finding.lineContent.trim()) {
        const codeLabel = noColor ? 'Code:' : c(colors.dim, 'Code:');
        const code = finding.lineContent.trim().substring(0, 100);
        const codeText = noColor ? code : c(colors.white, code);
        output += `   ${codeLabel} ${codeText}${finding.lineContent.trim().length > 100 ? '...' : ''}\n`;
        output += '\n';
    }
    // Description
    output += noColor
        ? `   ${finding.description}\n`
        : `   ${c(colors.dim, finding.description)}\n`;
    output += '\n';
    // Fix steps
    if (finding.fixSteps && finding.fixSteps.length > 0) {
        const fixLabel = noColor ? 'How to fix:' : c(colors.green + colors.bold, 'How to fix:');
        output += `   ${fixLabel}\n`;
        finding.fixSteps.forEach((step, i) => {
            const stepText = noColor ? `${i + 1}. ${step}` : c(colors.green, `${i + 1}. ${step}`);
            output += `   ${stepText}\n`;
        });
        output += '\n';
    }
    else if (finding.suggestedFix) {
        const fixLabel = noColor ? 'Suggested fix:' : c(colors.green + colors.bold, 'Suggested fix:');
        output += `   ${fixLabel} ${finding.suggestedFix}\n`;
        output += '\n';
    }
    // Verbose mode: additional details
    if (verbose) {
        // References
        if (finding.references && finding.references.length > 0) {
            const refLabel = noColor ? 'References:' : c(colors.blue, 'References:');
            output += `   ${refLabel}\n`;
            finding.references.forEach(ref => {
                output += noColor
                    ? `   - ${ref}\n`
                    : `   ${c(colors.blue, `- ${ref}`)}\n`;
            });
            output += '\n';
        }
        // Validation notes
        if (finding.validationNotes) {
            const notesLabel = noColor ? '[AI]' : c(colors.magenta, '[AI]');
            output += `   ${notesLabel} ${finding.validationNotes}\n`;
            output += '\n';
        }
        // Category and confidence
        output += noColor
            ? `   Category: ${finding.category} · Confidence: ${finding.confidence || 'medium'} · Layer: ${finding.layer}\n`
            : c(colors.dim, `   Category: ${finding.category} · Confidence: ${finding.confidence || 'medium'} · Layer: ${finding.layer}\n`);
    }
    return output;
}
/**
 * Format as simple list (no grouping, no colors)
 */
function formatSimpleList(vulnerabilities) {
    let output = '';
    for (const finding of vulnerabilities) {
        const severity = finding.severity.toUpperCase().padEnd(8);
        output += `[${severity}] ${finding.filePath}:${finding.lineNumber} - ${finding.title}\n`;
    }
    return output;
}
/**
 * Format as JSON (for piping to other tools)
 */
function formatJSON(result, pretty = false) {
    if (pretty) {
        return JSON.stringify(result, null, 2);
    }
    return JSON.stringify(result);
}
/**
 * Rule metadata for SARIF output
 */
const RULE_METADATA = {
    hardcoded_secret: {
        name: 'Hardcoded Secret',
        description: 'Sensitive credentials or API keys hardcoded in source code. These can be extracted from version control history or compiled binaries.',
        helpUri: 'https://oculum.dev/docs/rules/hardcoded-secrets',
        tags: ['security', 'secrets', 'credentials'],
    },
    high_entropy_string: {
        name: 'High Entropy String',
        description: 'A high-entropy string that may be a secret or API key. Review to ensure it is not sensitive data.',
        helpUri: 'https://oculum.dev/docs/rules/high-entropy',
        tags: ['security', 'secrets'],
    },
    ai_prompt_injection: {
        name: 'AI Prompt Injection',
        description: 'User input is included in AI prompts without proper sanitization, potentially allowing prompt injection attacks.',
        helpUri: 'https://oculum.dev/docs/rules/prompt-injection',
        tags: ['security', 'ai', 'injection'],
    },
    ai_unsafe_execution: {
        name: 'AI Unsafe Execution',
        description: 'AI-generated content is used in code execution, SQL queries, or other dangerous sinks without validation.',
        helpUri: 'https://oculum.dev/docs/rules/unsafe-execution',
        tags: ['security', 'ai', 'injection'],
    },
    ai_overpermissive_tool: {
        name: 'AI Overpermissive Tool',
        description: 'AI agent tool has excessive permissions without proper restrictions or sandboxing.',
        helpUri: 'https://oculum.dev/docs/rules/overpermissive-tools',
        tags: ['security', 'ai', 'authorization'],
    },
    ai_rag_exfiltration: {
        name: 'AI RAG Data Exfiltration',
        description: 'RAG (Retrieval Augmented Generation) queries may expose data across tenant boundaries or leak sensitive context.',
        helpUri: 'https://oculum.dev/docs/rules/rag-exfiltration',
        tags: ['security', 'ai', 'data-exposure'],
    },
    ai_endpoint_unprotected: {
        name: 'AI Endpoint Unprotected',
        description: 'AI endpoint lacks authentication or rate limiting, potentially allowing abuse or cost attacks.',
        helpUri: 'https://oculum.dev/docs/rules/unprotected-endpoints',
        tags: ['security', 'ai', 'authentication'],
    },
    ai_schema_mismatch: {
        name: 'AI Schema Validation Missing',
        description: 'AI-generated output is used without schema validation, potentially allowing malformed or malicious data.',
        helpUri: 'https://oculum.dev/docs/rules/schema-validation',
        tags: ['security', 'ai', 'validation'],
    },
    sql_injection: {
        name: 'SQL Injection',
        description: 'User input is concatenated into SQL queries without parameterization, allowing SQL injection attacks.',
        helpUri: 'https://oculum.dev/docs/rules/sql-injection',
        tags: ['security', 'injection', 'database'],
    },
    xss: {
        name: 'Cross-Site Scripting (XSS)',
        description: 'User input is rendered in HTML without proper escaping, allowing script injection.',
        helpUri: 'https://oculum.dev/docs/rules/xss',
        tags: ['security', 'injection', 'web'],
    },
    command_injection: {
        name: 'Command Injection',
        description: 'User input is passed to shell commands without sanitization, allowing arbitrary command execution.',
        helpUri: 'https://oculum.dev/docs/rules/command-injection',
        tags: ['security', 'injection', 'shell'],
    },
    missing_auth: {
        name: 'Missing Authentication',
        description: 'Sensitive endpoint or route lacks authentication checks.',
        helpUri: 'https://oculum.dev/docs/rules/missing-auth',
        tags: ['security', 'authentication'],
    },
    data_exposure: {
        name: 'Data Exposure',
        description: 'Sensitive data may be exposed through logging, error messages, or API responses.',
        helpUri: 'https://oculum.dev/docs/rules/data-exposure',
        tags: ['security', 'data-exposure'],
    },
    insecure_config: {
        name: 'Insecure Configuration',
        description: 'Security-relevant configuration is set to an insecure value.',
        helpUri: 'https://oculum.dev/docs/rules/insecure-config',
        tags: ['security', 'configuration'],
    },
    dangerous_function: {
        name: 'Dangerous Function',
        description: 'Use of a function known to be dangerous or deprecated for security reasons.',
        helpUri: 'https://oculum.dev/docs/rules/dangerous-functions',
        tags: ['security', 'code-quality'],
    },
};
/**
 * Format as SARIF (Static Analysis Results Interchange Format)
 * For integration with GitHub Code Scanning
 */
function formatSARIF(result) {
    // Build results from active vulnerabilities
    const activeResults = result.vulnerabilities.map((v, index) => ({
        ruleId: v.category,
        ruleIndex: getRuleIndex(result.vulnerabilities, v.category),
        level: mapSeverityToSARIF(v.severity),
        message: {
            text: v.description,
        },
        locations: [{
                physicalLocation: {
                    artifactLocation: {
                        uri: v.filePath,
                        uriBaseId: '%SRCROOT%',
                    },
                    region: {
                        startLine: v.lineNumber,
                        startColumn: 1,
                        snippet: v.lineContent ? { text: v.lineContent } : undefined,
                    },
                },
            }],
        fingerprints: {
            'oculum/v1': `${v.category}:${v.filePath}:${v.lineNumber}`,
        },
        fixes: v.suggestedFix ? [{
                description: {
                    text: v.suggestedFix,
                },
            }] : undefined,
        properties: {
            confidence: v.confidence,
            layer: v.layer,
        },
    }));
    // Build results from suppressed vulnerabilities (with SARIF suppression state)
    const suppressedResults = (result.suppressedVulnerabilities || []).map((s) => ({
        ruleId: s.category,
        ruleIndex: 0, // Will be resolved by GitHub
        level: mapSeverityToSARIF(s.severity),
        message: {
            text: s.title,
        },
        locations: [{
                physicalLocation: {
                    artifactLocation: {
                        uri: s.filePath,
                        uriBaseId: '%SRCROOT%',
                    },
                    region: {
                        startLine: s.lineNumber,
                        startColumn: 1,
                    },
                },
            }],
        fingerprints: {
            'oculum/v1': `${s.category}:${s.filePath}:${s.lineNumber}`,
            'oculum/hash': s.hash,
        },
        suppressions: [{
                kind: s.suppressionType === 'inline' ? 'inSource' : 'external',
                justification: s.suppressionReason,
                state: 'accepted',
            }],
        properties: {
            suppressionType: s.suppressionType,
            expires: s.expires,
        },
    }));
    return {
        $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
        version: '2.1.0',
        runs: [{
                tool: {
                    driver: {
                        name: 'Oculum',
                        version: '1.0.0',
                        informationUri: 'https://oculum.dev',
                        organization: 'Oculum Security',
                        rules: getUniqueRules(result.vulnerabilities),
                    },
                },
                results: [...activeResults, ...suppressedResults],
                columnKind: 'utf16CodeUnits',
            }],
    };
}
function mapSeverityToSARIF(severity) {
    switch (severity) {
        case 'critical':
        case 'high':
            return 'error';
        case 'medium':
            return 'warning';
        default:
            return 'note';
    }
}
function getRuleIndex(vulnerabilities, category) {
    const seen = new Set();
    let index = 0;
    for (const v of vulnerabilities) {
        if (!seen.has(v.category)) {
            if (v.category === category)
                return index;
            seen.add(v.category);
            index++;
        }
    }
    return 0;
}
function getUniqueRules(vulnerabilities) {
    const seen = new Set();
    const rules = [];
    for (const v of vulnerabilities) {
        if (seen.has(v.category))
            continue;
        seen.add(v.category);
        const metadata = RULE_METADATA[v.category];
        const ruleName = metadata?.name || v.category.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
        rules.push({
            id: v.category,
            name: ruleName,
            shortDescription: { text: ruleName },
            fullDescription: {
                text: metadata?.description || v.description,
            },
            helpUri: metadata?.helpUri || `https://oculum.dev/docs/rules/${v.category.replace(/_/g, '-')}`,
            help: {
                text: metadata?.description || v.description,
                markdown: `# ${ruleName}\n\n${metadata?.description || v.description}\n\n[Learn more](${metadata?.helpUri || 'https://oculum.dev/docs'})`,
            },
            defaultConfiguration: {
                level: mapSeverityToSARIF(v.severity),
            },
            properties: {
                tags: metadata?.tags || ['security'],
                precision: v.confidence === 'high' ? 'high' : v.confidence === 'medium' ? 'medium' : 'low',
                'security-severity': mapSeverityToScore(v.severity),
            },
        });
    }
    return rules;
}
function mapSeverityToScore(severity) {
    switch (severity) {
        case 'critical':
            return '9.0';
        case 'high':
            return '7.0';
        case 'medium':
            return '5.0';
        case 'low':
            return '3.0';
        default:
            return '1.0';
    }
}
//# sourceMappingURL=cli-terminal.js.map