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
199 lines
6.9 KiB
JavaScript
199 lines
6.9 KiB
JavaScript
// sarif.test.mjs — Tests for SARIF 2.1.0 output formatter
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { toSARIF } from '../../scanners/lib/sarif-formatter.mjs';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fixture: minimal scan-orchestrator envelope
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const EMPTY_ENVELOPE = {
|
|
meta: { target: '/tmp/test', timestamp: '2026-04-10T12:00:00.000Z' },
|
|
scanners: {},
|
|
aggregate: { total_findings: 0, counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 } },
|
|
};
|
|
|
|
const ENVELOPE_WITH_FINDINGS = {
|
|
meta: { target: '/tmp/test', timestamp: '2026-04-10T12:00:00.000Z' },
|
|
scanners: {
|
|
unicode: {
|
|
scanner: 'unicode',
|
|
status: 'ok',
|
|
findings: [
|
|
{
|
|
id: 'DS-UNI-001',
|
|
scanner: 'UNI',
|
|
severity: 'critical',
|
|
title: 'Invisible Unicode characters detected',
|
|
description: 'File contains zero-width joiners that may hide malicious intent.',
|
|
file: 'src/hook.mjs',
|
|
line: 42,
|
|
owasp: 'LLM01',
|
|
recommendation: 'Remove invisible characters.',
|
|
},
|
|
{
|
|
id: 'DS-UNI-002',
|
|
scanner: 'UNI',
|
|
severity: 'medium',
|
|
title: 'Homoglyph characters detected',
|
|
description: 'Cyrillic characters mixed with Latin.',
|
|
file: 'src/config.mjs',
|
|
line: null,
|
|
owasp: 'LLM01',
|
|
recommendation: 'Replace with ASCII equivalents.',
|
|
},
|
|
],
|
|
counts: { critical: 1, high: 0, medium: 1, low: 0, info: 0 },
|
|
},
|
|
entropy: {
|
|
scanner: 'entropy',
|
|
status: 'ok',
|
|
findings: [
|
|
{
|
|
id: 'DS-ENT-001',
|
|
scanner: 'ENT',
|
|
severity: 'high',
|
|
title: 'High-entropy string detected',
|
|
description: 'Possible hardcoded API key.',
|
|
file: '.env.example',
|
|
line: 5,
|
|
owasp: 'LLM03',
|
|
recommendation: 'Move secrets to environment variables.',
|
|
},
|
|
],
|
|
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: 4 },
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SARIF structure tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('sarif-formatter: structure', () => {
|
|
it('produces valid SARIF 2.1.0 shell', () => {
|
|
const sarif = toSARIF(EMPTY_ENVELOPE);
|
|
assert.equal(sarif.version, '2.1.0');
|
|
assert.ok(sarif.$schema.includes('sarif-schema-2.1.0'));
|
|
assert.ok(Array.isArray(sarif.runs));
|
|
assert.equal(sarif.runs.length, 1);
|
|
});
|
|
|
|
it('has tool driver info', () => {
|
|
const sarif = toSARIF(EMPTY_ENVELOPE);
|
|
const driver = sarif.runs[0].tool.driver;
|
|
assert.equal(driver.name, 'llm-security');
|
|
assert.ok(driver.version);
|
|
assert.ok(driver.informationUri);
|
|
});
|
|
|
|
it('empty findings produce empty results array', () => {
|
|
const sarif = toSARIF(EMPTY_ENVELOPE);
|
|
assert.deepEqual(sarif.runs[0].results, []);
|
|
assert.deepEqual(sarif.runs[0].tool.driver.rules, []);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Findings conversion tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('sarif-formatter: findings', () => {
|
|
it('converts all findings to results', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
assert.equal(sarif.runs[0].results.length, 4);
|
|
});
|
|
|
|
it('maps critical/high severity to error level', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const critResult = sarif.runs[0].results[0]; // critical
|
|
const highResult = sarif.runs[0].results[2]; // high
|
|
assert.equal(critResult.level, 'error');
|
|
assert.equal(highResult.level, 'error');
|
|
});
|
|
|
|
it('maps medium severity to warning level', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const medResult = sarif.runs[0].results[1]; // medium
|
|
assert.equal(medResult.level, 'warning');
|
|
});
|
|
|
|
it('includes file as artifact location URI', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const result = sarif.runs[0].results[0];
|
|
assert.equal(result.locations[0].physicalLocation.artifactLocation.uri, 'src/hook.mjs');
|
|
});
|
|
|
|
it('includes line number as region startLine', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const result = sarif.runs[0].results[0];
|
|
assert.equal(result.locations[0].physicalLocation.region.startLine, 42);
|
|
});
|
|
|
|
it('omits region when line is null', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const result = sarif.runs[0].results[1]; // line: null
|
|
assert.ok(result.locations[0].physicalLocation.artifactLocation);
|
|
assert.equal(result.locations[0].physicalLocation.region, undefined);
|
|
});
|
|
|
|
it('includes OWASP tags in properties', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const result = sarif.runs[0].results[0];
|
|
assert.deepEqual(result.properties.tags, ['LLM01']);
|
|
});
|
|
|
|
it('generates unique rules from findings', () => {
|
|
const sarif = toSARIF(ENVELOPE_WITH_FINDINGS);
|
|
const rules = sarif.runs[0].tool.driver.rules;
|
|
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) {
|
|
assert.ok(typeof result.ruleIndex === 'number');
|
|
assert.ok(result.ruleIndex >= 0);
|
|
assert.ok(result.ruleIndex < sarif.runs[0].tool.driver.rules.length);
|
|
}
|
|
});
|
|
|
|
it('accepts custom version', () => {
|
|
const sarif = toSARIF(EMPTY_ENVELOPE, '7.0.0');
|
|
assert.equal(sarif.runs[0].tool.driver.version, '7.0.0');
|
|
});
|
|
});
|