diff --git a/src/__tests__/sarif.test.ts b/src/__tests__/sarif.test.ts index 92458bc..c3c00d0 100644 --- a/src/__tests__/sarif.test.ts +++ b/src/__tests__/sarif.test.ts @@ -134,6 +134,31 @@ describe('toSarif()', () => { expect(sarif.runs[0].tool.driver.rules[0].id).toBe(RuleId.NO_AUTH); }); + it('rules carry a GitHub "security-severity" property', () => { + const finding = makeFinding({ ruleId: RuleId.NO_AUTH, severity: Severity.CRITICAL }); + const sarif = toSarif(makeReport({ findings: [finding] })); + expect(sarif.runs[0].tool.driver.rules[0].properties).toEqual({ + 'security-severity': '9.0', + }); + }); + + it('security-severity score maps to GitHub severity bands', () => { + const cases: Array<[Severity, string]> = [ + [Severity.CRITICAL, '9.0'], // >= 9.0 → critical + [Severity.HIGH, '8.0'], // 7.0–8.9 → high + [Severity.MEDIUM, '5.0'], // 4.0–6.9 → medium + [Severity.LOW, '2.0'], // < 4.0 → low + [Severity.INFO, '0.0'], + ]; + for (const [severity, score] of cases) { + const finding = makeFinding({ ruleId: RuleId.NO_AUTH, severity }); + const sarif = toSarif(makeReport({ findings: [finding] })); + expect(sarif.runs[0].tool.driver.rules[0].properties).toEqual({ + 'security-severity': score, + }); + } + }); + it('configPath is included as artifact URI when provided', () => { const finding = makeFinding({ ruleId: RuleId.NO_AUTH, severity: Severity.CRITICAL }); const sarif = toSarif(makeReport({ findings: [finding] }), './mcp-config.json'); diff --git a/src/sarif.ts b/src/sarif.ts index 0abc682..0d79d0f 100644 --- a/src/sarif.ts +++ b/src/sarif.ts @@ -30,6 +30,7 @@ interface SarifRule { name: string; shortDescription: { text: string }; helpUri?: string; + properties?: { 'security-severity': string }; } interface SarifResult { @@ -67,6 +68,32 @@ function severityToLevel(severity: Severity): 'error' | 'warning' | 'note' { } } +/** + * Map a Severity to a GitHub Code Scanning `security-severity` score. + * + * GitHub reads this numeric string (0.0–10.0) from each rule's `properties` + * bag to assign the Critical/High/Medium/Low badge in the Security tab and to + * drive severity-based alert filtering and branch-protection rules. Without it, + * GitHub can only rank by SARIF `level`, so CRITICAL and HIGH (both `error`) + * become indistinguishable. The bands follow GitHub's documented thresholds: + * 9.0+ → critical, 7.0–8.9 → high, 4.0–6.9 → medium, <4.0 → low. + */ +function severityToScore(severity: Severity): string { + switch (severity) { + case Severity.CRITICAL: + return '9.0'; + case Severity.HIGH: + return '8.0'; + case Severity.MEDIUM: + return '5.0'; + case Severity.LOW: + return '2.0'; + case Severity.INFO: + default: + return '0.0'; + } +} + /** * Convert a SecurityReport to a minimal SARIF 2.1.0 document. * @@ -83,6 +110,7 @@ export function toSarif(report: SecurityReport, configPath?: string): SarifOutpu name: finding.title, shortDescription: { text: finding.title }, helpUri: finding.docsUrl, + properties: { 'security-severity': severityToScore(finding.severity) }, }); } }