diff --git a/src/__tests__/reporter.test.ts b/src/__tests__/reporter.test.ts index 7cb1553..cfc5a9d 100644 --- a/src/__tests__/reporter.test.ts +++ b/src/__tests__/reporter.test.ts @@ -37,4 +37,24 @@ describe('renderReport', () => { it('throws on unsupported format', () => { expect(() => renderReport(sampleReport, 'xml' as never)).toThrow(); }); + + it('escapes pipes and newlines in markdown cells so the table stays well-formed', () => { + const report: ChangeReport = { + added: [{ name: 'evil | pkg', version: '1.0', ecosystem: 'npm' }], + removed: [], + upgraded: [], + newCVEs: [{ id: 'CVE-2024-0001', affects: 'pkg:npm/a | b', severity: 'high', description: 'line1\nline2' }], + fixedCVEs: [], + summary: { totalAdded: 1, totalRemoved: 0, totalUpgraded: 0, totalNewCVEs: 1, totalFixedCVEs: 0 }, + }; + const out = renderReport(report, 'markdown'); + + // The pipe in the package name must be escaped, not interpreted as a column break. + expect(out).toContain('| evil \\| pkg | 1.0 | npm |'); + expect(out).toContain('| CVE-2024-0001 | high | pkg:npm/a \\| b |'); + + // Every body row under the Added table must keep its column count (4 leading bars: 3 cells). + const addedRow = out.split('\n').find(l => l.includes('evil'))!; + expect((addedRow.match(/(? 0) { lines.push('## \u2796 Removed Components', ''); lines.push('| Name | Version |'); lines.push('|------|---------|'); - for (const c of r.removed) lines.push(`| ${c.name} | ${c.version ?? '\u2014'} |`); + for (const c of r.removed) lines.push(`| ${escapeCell(c.name)} | ${escapeCell(c.version)} |`); lines.push(''); } if (r.upgraded.length > 0) { @@ -95,7 +110,7 @@ function renderMarkdown(r: ChangeReport): string { lines.push('| Name | From | To | Major? |'); lines.push('|------|------|----|--------|'); for (const u of r.upgraded) { - lines.push(`| ${u.component.name} | ${u.from} | ${u.to} | ${u.isMajorBump ? '\u26a0\ufe0f Yes' : 'No'} |`); + lines.push(`| ${escapeCell(u.component.name)} | ${escapeCell(u.from)} | ${escapeCell(u.to)} | ${u.isMajorBump ? '\u26a0\ufe0f Yes' : 'No'} |`); } lines.push(''); } @@ -103,14 +118,14 @@ function renderMarkdown(r: ChangeReport): string { lines.push('## \ud83d\udea8 New CVEs', ''); lines.push('| CVE ID | Severity | Affects |'); lines.push('|--------|----------|---------|'); - for (const v of r.newCVEs) lines.push(`| ${v.id} | ${v.severity ?? '\u2014'} | ${v.affects} |`); + for (const v of r.newCVEs) lines.push(`| ${escapeCell(v.id)} | ${escapeCell(v.severity)} | ${escapeCell(v.affects)} |`); lines.push(''); } if (r.fixedCVEs.length > 0) { lines.push('## \u2705 Fixed CVEs', ''); lines.push('| CVE ID | Affects |'); lines.push('|--------|---------|'); - for (const v of r.fixedCVEs) lines.push(`| ${v.id} | ${v.affects} |`); + for (const v of r.fixedCVEs) lines.push(`| ${escapeCell(v.id)} | ${escapeCell(v.affects)} |`); } return lines.join('\n');