// sarif-formatter.mjs — Converts scan-orchestrator envelope to SARIF 2.1.0 // OASIS SARIF standard: https://docs.oasis-open.org/sarif/sarif/v2.1.0/ // Zero external dependencies. const SARIF_SCHEMA = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json'; const SARIF_VERSION = '2.1.0'; const TOOL_NAME = 'llm-security'; const TOOL_URI = 'https://git.fromaitochitta.com/open/claude-code-llm-security'; /** * Map finding severity to SARIF level. * @param {string} severity - critical|high|medium|low|info * @returns {string} SARIF level: error|warning|note */ function toLevel(severity) { switch (severity) { case 'critical': case 'high': return 'error'; case 'medium': return 'warning'; case 'low': case 'info': default: return 'note'; } } /** * Build SARIF rules array from unique finding scanner+title combos. * @param {object[]} findings * @returns {{ rules: object[], ruleIndex: Map }} */ function buildRules(findings) { const ruleIndex = new Map(); const rules = []; for (const f of findings) { const ruleId = `${f.scanner}/${f.title.replace(/\s+/g, '-').toLowerCase()}`; if (!ruleIndex.has(ruleId)) { ruleIndex.set(ruleId, rules.length); rules.push({ id: ruleId, name: f.title, shortDescription: { text: f.title }, fullDescription: { text: f.description || f.title }, defaultConfiguration: { level: toLevel(f.severity) }, properties: { tags: f.owasp ? [f.owasp] : [], }, }); } } return { rules, ruleIndex }; } /** * Convert scan-orchestrator envelope JSON to SARIF 2.1.0 format. * @param {object} envelopeData - The full scan-orchestrator output * @param {string} [version='6.0.0'] - Tool version * @returns {object} SARIF 2.1.0 JSON */ export function toSARIF(envelopeData, version = '6.0.0') { // Collect all findings from all scanners const allFindings = []; if (envelopeData.scanners) { for (const scannerResult of Object.values(envelopeData.scanners)) { if (scannerResult.findings) { allFindings.push(...scannerResult.findings); } } } const { rules, ruleIndex } = buildRules(allFindings); // Build SARIF results const results = allFindings.map(f => { const ruleId = `${f.scanner}/${f.title.replace(/\s+/g, '-').toLowerCase()}`; const result = { ruleId, ruleIndex: ruleIndex.get(ruleId), level: toLevel(f.severity), message: { text: f.description || f.title }, properties: {}, }; // Add OWASP tags if (f.owasp) { result.properties.tags = [f.owasp]; } // Add recommendation if (f.recommendation) { result.properties.recommendation = f.recommendation; } // Add location if file is present if (f.file) { const location = { physicalLocation: { artifactLocation: { uri: f.file }, }, }; if (f.line) { location.physicalLocation.region = { startLine: f.line }; } result.locations = [location]; } return result; }); return { $schema: SARIF_SCHEMA, version: SARIF_VERSION, runs: [{ tool: { driver: { name: TOOL_NAME, version, informationUri: TOOL_URI, rules, }, }, results, }], }; }