diff --git a/__tests__/summary.test.ts b/__tests__/summary.test.ts index eec1f4b80..44b92f573 100644 --- a/__tests__/summary.test.ts +++ b/__tests__/summary.test.ts @@ -696,6 +696,49 @@ test('addChangeVulnerabilitiesToSummary() - handles RestSharp GHSA-4rr6-2v9v-wcp }) }) +test('addChangeVulnerabilitiesToSummary() - handles GHSA-vc5p-v9hr-52mj maven range', async () => { + const pkg = createTestChange({ + ecosystem: 'maven', + name: 'org.apache.logging.log4j:log4j-core', + version: '2.12.4', + vulnerabilities: [ + createTestVulnerability({ + advisory_ghsa_id: 'GHSA-vc5p-v9hr-52mj', + advisory_summary: 'Apache Log4j vulnerable to XXE in JDBC Appender', + severity: 'high' + }) + ] + }) + + // Mock API response from https://api.github.com/advisories/GHSA-vc5p-v9hr-52mj + mockOctokitRequest.mockResolvedValueOnce({ + data: { + vulnerabilities: [ + { + package: { + ecosystem: 'maven', + name: 'org.apache.logging.log4j:log4j-core' + }, + vulnerable_version_range: '>= 2.0-beta9, < 2.25.3', + first_patched_version: '2.25.3' + } + ] + } + }) + + const changes = [pkg] + await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true) + + const text = core.summary.stringify() + + // Should show the correct patched version for this advisory range + expect(text).toContain('2.25.3') + expect(text).not.toContain('N/A') + expect(mockOctokitRequest).toHaveBeenCalledWith('GET /advisories/{ghsa_id}', { + ghsa_id: 'GHSA-vc5p-v9hr-52mj' + }) +}) + test('addChangeVulnerabilitiesToSummary() - handles version coercion for non-strict semver versions', async () => { // Test that versions like "8.0" (without patch version) can be coerced to "8.0.0" // for successful range matching in fail-open mode (patch selection) @@ -778,6 +821,50 @@ test('addChangeVulnerabilitiesToSummary() - handles invalid versions in fail-ope expect(text).toContain('N/A') }) +test('addChangeVulnerabilitiesToSummary() - handles GHSA-r9w3-57w2-gch2 go pseudo-version range', async () => { + const pkg = createTestChange({ + ecosystem: 'go', + name: 'github.com/ory/hydra', + version: '2.1.0-pre.2', + vulnerabilities: [ + createTestVulnerability({ + advisory_ghsa_id: 'GHSA-r9w3-57w2-gch2', + advisory_summary: + 'Ory Hydra has a SQL injection via forged pagination tokens', + severity: 'high' + }) + ] + }) + + // Mock API response from https://api.github.com/advisories/GHSA-r9w3-57w2-gch2 + mockOctokitRequest.mockResolvedValueOnce({ + data: { + vulnerabilities: [ + { + package: { + ecosystem: 'go', + name: 'github.com/ory/hydra' + }, + vulnerable_version_range: '< 2.3.1-0.20260320110106-0b84568fffcc', + first_patched_version: '2.3.1-0.20260320110106-0b84568fffcc' + } + ] + } + }) + + const changes = [pkg] + await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true) + + const text = core.summary.stringify() + + // Should show the correct patched version for this advisory range + expect(text).toContain('2.3.1-0.20260320110106-0b84568fffcc') + expect(text).not.toContain('N/A') + expect(mockOctokitRequest).toHaveBeenCalledWith('GET /advisories/{ghsa_id}', { + ghsa_id: 'GHSA-r9w3-57w2-gch2' + }) +}) + test('addChangeVulnerabilitiesToSummary() - respects concurrency limit for API calls', async () => { // Create 15 packages with different vulnerabilities to test concurrency limiting const packages = Array.from({length: 15}, (_, i) => diff --git a/src/summary.ts b/src/summary.ts index 892b38e87..28d2b0ee6 100644 --- a/src/summary.ts +++ b/src/summary.ts @@ -74,7 +74,7 @@ function versionInRange( // Validate version and range explicitly to enforce fail-closed semantics // semver.satisfies() typically returns false for invalid inputs without throwing let validVersion = semver.valid(trimmedVersion) - const validRange = semver.validRange(semverRange) + let validRange = semver.validRange(semverRange) // For fail-open mode (patch selection), try coercing invalid versions // to handle common real-world formats like "8.0", date-based versions, etc. @@ -88,6 +88,26 @@ function versionInRange( } } + // For fail-open mode, try coercing invalid version components within the range string + // to handle formats like "2.0-beta9" which should be "2.0.0-beta9" for strict semver + if (!validRange && !failClosed) { + const coercedRangeStr = semverRange.replace( + /(?<=[><=~^\s]|^)(\d+\.\d+(?:\.\d+)?(?:-[^\s]*)?)(?=\s|$)/g, + match => { + const coerced = semver.coerce(match) + return coerced ? coerced.version : match + } + ) + if (coercedRangeStr !== semverRange) { + validRange = semver.validRange(coercedRangeStr) + if (validRange) { + core.debug( + `Coerced range "${semverRange}" to "${coercedRangeStr}" for range matching` + ) + } + } + } + if (!validVersion || !validRange) { if (failClosed) { const issues: string[] = []