fix(llm-security): sarif-formatter splits comma-separated owasp string into multiple tags

buildRules() and toSARIF() wrapped a multi-mapping f.owasp string
(e.g. 'MCP03, MCP06', emitted by mcp-live-inspect.mjs and
ide-extension-scanner.mjs) as a single-element tags array instead of
splitting it, silently dropping the second OWASP mapping in SARIF
output. Added owaspTags() helper; test asserts a multi-entry tags
array for both rule and result properties.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbQmoLxcAAsJAkFxBeCx6z
This commit is contained in:
Kjell Tore Guttormsen 2026-08-18 16:52:52 +02:00
commit e97c23246e
2 changed files with 44 additions and 5 deletions

View file

@ -26,6 +26,15 @@ function toLevel(severity) {
}
}
/**
* Split a possibly comma-separated OWASP string (e.g. 'MCP03, MCP06') into tags.
* @param {string} [owasp]
* @returns {string[]}
*/
function owaspTags(owasp) {
return owasp ? owasp.split(',').map(s => s.trim()).filter(Boolean) : [];
}
/**
* Build SARIF rules array from unique finding scanner+title combos.
* @param {object[]} findings
@ -46,7 +55,7 @@ function buildRules(findings) {
fullDescription: { text: f.description || f.title },
defaultConfiguration: { level: toLevel(f.severity) },
properties: {
tags: f.owasp ? [f.owasp] : [],
tags: owaspTags(f.owasp),
},
});
}
@ -87,7 +96,7 @@ export function toSARIF(envelopeData, version = '6.0.0') {
// Add OWASP tags
if (f.owasp) {
result.properties.tags = [f.owasp];
result.properties.tags = owaspTags(f.owasp);
}
// Add recommendation

View file

@ -64,8 +64,26 @@ const ENVELOPE_WITH_FINDINGS = {
],
counts: { critical: 0, high: 1, medium: 0, low: 0, info: 0 },
},
mcp: {
scanner: 'mcp',
status: 'ok',
findings: [
{
id: 'DS-MCP-001',
scanner: 'MCP',
severity: 'high',
title: 'Tool description drift detected',
description: 'MCP tool description changed after install.',
file: null,
line: null,
owasp: 'MCP03, MCP06',
recommendation: 'Review the tool description change.',
},
],
counts: { critical: 0, high: 1, medium: 0, low: 0, info: 0 },
},
},
aggregate: { total_findings: 3 },
aggregate: { total_findings: 4 },
};
// ---------------------------------------------------------------------------
@ -103,7 +121,7 @@ describe('sarif-formatter: structure', () => {
describe('sarif-formatter: findings', () => {
it('converts all findings to results', () => {
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
assert.equal(sarif.runs[0].results.length, 3);
assert.equal(sarif.runs[0].results.length, 4);
});
it('maps critical/high severity to error level', () => {
@ -148,11 +166,23 @@ describe('sarif-formatter: findings', () => {
it('generates unique rules from findings', () => {
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
const rules = sarif.runs[0].tool.driver.rules;
assert.equal(rules.length, 3); // 3 unique title+scanner combos
assert.equal(rules.length, 4); // 4 unique title+scanner combos
assert.ok(rules[0].id.startsWith('UNI/'));
assert.ok(rules[2].id.startsWith('ENT/'));
});
it('splits comma-separated owasp string into multiple result tags', () => {
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
const mcpResult = sarif.runs[0].results[3]; // owasp: 'MCP03, MCP06'
assert.deepEqual(mcpResult.properties.tags, ['MCP03', 'MCP06']);
});
it('splits comma-separated owasp string into multiple rule tags', () => {
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
const mcpRule = sarif.runs[0].tool.driver.rules[3];
assert.deepEqual(mcpRule.properties.tags, ['MCP03', 'MCP06']);
});
it('results reference correct rule index', () => {
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
for (const result of sarif.runs[0].results) {