Skip to content

Commit 01d2bdb

Browse files
committed
fix: prefer closest fix
1 parent 7068e26 commit 01d2bdb

2 files changed

Lines changed: 79 additions & 5 deletions

File tree

server/utils/dependency-analysis.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ function parseRangeIntervals(range: OsvRange): Array<{ introduced: string; fixed
182182

183183
/**
184184
* Extract the fixed version for a specific package version from vulnerability data.
185-
* Finds the interval that contains the current version and returns its fixed version
186-
* Finds the interval that contains the current version and returns its fixed version.
185+
* Finds all intervals that contain the current version and returns the closest fix,
186+
* preferring a nearby backport over a distant major-version bump.
187187
* @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
188188
*/
189189
function getFixedVersion(
@@ -198,7 +198,9 @@ function getFixedVersion(
198198
a => a.package.ecosystem === 'npm' && a.package.name === packageName,
199199
)
200200

201-
// Check each entry's ranges to find the interval that contains the current version
201+
// Collect all matching fixed versions across all ranges
202+
const matchingFixedVersions: string[] = []
203+
202204
for (const entry of packageAffectedEntries) {
203205
if (!entry.ranges) continue
204206

@@ -213,7 +215,7 @@ function getFixedVersion(
213215
const afterIntro = semver.gte(currentVersion, introVersion)
214216
const beforeFixed = !interval.fixed || semver.lt(currentVersion, interval.fixed)
215217
if (afterIntro && beforeFixed && interval.fixed) {
216-
return interval.fixed
218+
matchingFixedVersions.push(interval.fixed)
217219
}
218220
} catch {
219221
continue
@@ -222,7 +224,11 @@ function getFixedVersion(
222224
}
223225
}
224226

225-
return undefined
227+
if (matchingFixedVersions.length === 0) return undefined
228+
if (matchingFixedVersions.length === 1) return matchingFixedVersions[0]
229+
230+
// Return the lowest (closest) fixed version — the smallest bump from the current version
231+
return matchingFixedVersions.sort(semver.compare)[0]
226232
}
227233

228234
function getSeverityLevel(vuln: OsvVulnerability): OsvSeverityLevel {

test/unit/server/utils/dependency-analysis.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,74 @@ describe('dependency-analysis', () => {
783783
expect(result.vulnerablePackages[0]?.vulnerabilities[0]?.fixedIn).toBe('1.6.0')
784784
})
785785

786+
it('suggests closest fixedIn when multiple ranges match (backport fix preferred)', async () => {
787+
const mockResolved = new Map([
788+
[
789+
'example@3.4.6',
790+
{
791+
name: 'example',
792+
version: '3.4.6',
793+
size: 1000,
794+
optional: false,
795+
depth: 'root' as const,
796+
path: ['example@3.4.6'],
797+
},
798+
],
799+
])
800+
vi.mocked(resolveDependencyTree).mockResolvedValue(mockResolved)
801+
802+
// Two affected ranges:
803+
// Range 1 (broad): >= 3.4.5-foo.2, < 3.5.9-foo.15 (fix: 3.5.9-foo.15)
804+
// Range 2 (narrow backport): >= 3.4.5-foo.2, < 3.4.8 (fix: 3.4.8)
805+
//
806+
// Version 3.4.6 falls in BOTH ranges.
807+
// The broad range is listed first, so the current early-return picks 3.5.9-foo.15.
808+
// But 3.4.8 is the closer/more appropriate fix for someone on 3.4.x.
809+
mockOsvApi(
810+
[{ vulns: [{ id: 'GHSA-backport', modified: '2024-01-01' }] }],
811+
new Map([
812+
[
813+
'example@3.4.6',
814+
{
815+
vulns: [
816+
{
817+
id: 'GHSA-backport',
818+
summary: 'Vulnerability with backported fix',
819+
database_specific: { severity: 'HIGH' },
820+
affected: [
821+
{
822+
package: { ecosystem: 'npm', name: 'example' },
823+
ranges: [
824+
{
825+
type: 'SEMVER',
826+
events: [{ introduced: '3.4.5-foo.2' }, { fixed: '3.5.9-foo.15' }],
827+
},
828+
],
829+
},
830+
{
831+
package: { ecosystem: 'npm', name: 'example' },
832+
ranges: [
833+
{
834+
type: 'SEMVER',
835+
events: [{ introduced: '3.4.5-foo.2' }, { fixed: '3.4.8' }],
836+
},
837+
],
838+
},
839+
],
840+
},
841+
],
842+
},
843+
],
844+
]),
845+
)
846+
847+
const result = await analyzeDependencyTree('example', '3.4.6')
848+
849+
expect(result.vulnerablePackages).toHaveLength(1)
850+
// Should suggest 3.4.8 (the closest fix), not 3.5.9-foo.15
851+
expect(result.vulnerablePackages[0]?.vulnerabilities[0]?.fixedIn).toBe('3.4.8')
852+
})
853+
786854
it('returns undefined fixedIn when no matching range has a fixed version', async () => {
787855
const mockResolved = new Map([
788856
[

0 commit comments

Comments
 (0)