@@ -9,9 +9,11 @@ import type {
99 VulnerabilityTreeResult ,
1010 DeprecatedPackageInfo ,
1111 OsvAffected ,
12+ OsvRange ,
1213} from '#shared/types/dependency-analysis'
1314import { mapWithConcurrency } from '#shared/utils/async'
1415import { resolveDependencyTree } from './dependency-resolver'
16+ import * as semver from 'semver'
1517
1618/** Maximum concurrent requests for fetching vulnerability details */
1719const OSV_DETAIL_CONCURRENCY = 25
@@ -116,7 +118,7 @@ async function queryOsvDetails(pkg: PackageQueryInfo): Promise<PackageVulnerabil
116118 severity,
117119 aliases : vuln . aliases || [ ] ,
118120 url : getVulnerabilityUrl ( vuln ) ,
119- fixedIn : getFixedVersion ( vuln . affected , pkg . name ) ,
121+ fixedIn : getFixedVersion ( vuln . affected , pkg . name , pkg . version ) ,
120122 } )
121123 }
122124
@@ -147,26 +149,56 @@ function getVulnerabilityUrl(vuln: OsvVulnerability): string {
147149}
148150
149151/**
150- * Extract the earliest fixed version for a specific package from vulnerability data.
151- * Returns the first 'fixed' event found in the affected ranges for the given package.
152+ * Check if a version falls within an OSV range (between introduced and fixed).
153+ * OSV ranges use events: introduced starts vulnerability, fixed ends it.
154+ */
155+ function isVersionInRange ( version : string , range : OsvRange ) : boolean {
156+ const introduced = range . events . find ( e => e . introduced ) ?. introduced
157+ const fixed = range . events . find ( e => e . fixed ) ?. fixed
158+
159+ if ( ! introduced ) return false
160+
161+ // Handle "0" as "0.0.0" for semver comparison
162+ const introVersion = introduced === '0' ? '0.0.0' : introduced
163+
164+ try {
165+ // Version must be >= introduced AND < fixed (if fixed exists)
166+ return semver . gte ( version , introVersion ) && ( ! fixed || semver . lt ( version , fixed ) )
167+ } catch {
168+ // If semver parsing fails, skip this range
169+ return false
170+ }
171+ }
172+
173+ /**
174+ * Extract the fixed version for a specific package version from vulnerability data.
175+ * Finds the range that contains the current version and returns its fixed version.
176+ * @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
152177 */
153178function getFixedVersion (
154179 affected : OsvAffected [ ] | undefined ,
155180 packageName : string ,
181+ currentVersion : string ,
156182) : string | undefined {
157183 if ( ! affected ) return undefined
158184
159- // Find the affected entry for this specific package
160- const packageAffected = affected . find (
185+ // Find all affected entries for this specific package
186+ const packageAffectedEntries = affected . filter (
161187 a => a . package . ecosystem === 'npm' && a . package . name === packageName ,
162188 )
163- if ( ! packageAffected ?. ranges ) return undefined
164189
165- // Look through ranges to find a 'fixed' event
166- for ( const range of packageAffected . ranges ) {
167- for ( const event of range . events ) {
168- if ( event . fixed ) {
169- return event . fixed
190+ // Check each entry's ranges to find one that contains the current version
191+ for ( const entry of packageAffectedEntries ) {
192+ if ( ! entry . ranges ) continue
193+
194+ for ( const range of entry . ranges ) {
195+ // Only handle SEMVER ranges (most common for npm)
196+ if ( range . type !== 'SEMVER' ) continue
197+
198+ if ( isVersionInRange ( currentVersion , range ) ) {
199+ // Found the matching range - return its fixed version
200+ const fixedEvent = range . events . find ( e => e . fixed )
201+ if ( fixedEvent ?. fixed ) return fixedEvent . fixed
170202 }
171203 }
172204 }
0 commit comments