@@ -149,30 +149,41 @@ function getVulnerabilityUrl(vuln: OsvVulnerability): string {
149149}
150150
151151/**
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.
152+ * Parse OSV range events into introduced/fixed pairs.
153+ * OSV events form a timeline: [introduced, fixed, introduced, fixed, ...]
154+ * A single range can have multiple introduced/fixed pairs representing
155+ * periods where the vulnerability was active, was fixed, and was reintroduced.
156+ * @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
154157 */
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
158+ function parseRangeIntervals ( range : OsvRange ) : Array < { introduced : string ; fixed ?: string } > {
159+ const intervals : Array < { introduced : string ; fixed ?: string } > = [ ]
160+ let currentIntroduced : string | undefined
161+
162+ for ( const event of range . events ) {
163+ if ( event . introduced !== undefined ) {
164+ // Start a new interval (close previous open one if any)
165+ if ( currentIntroduced !== undefined ) {
166+ intervals . push ( { introduced : currentIntroduced } )
167+ }
168+ currentIntroduced = event . introduced
169+ } else if ( event . fixed !== undefined && currentIntroduced !== undefined ) {
170+ intervals . push ( { introduced : currentIntroduced , fixed : event . fixed } )
171+ currentIntroduced = undefined
172+ }
173+ }
163174
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
175+ // Handle trailing introduced with no fixed (still vulnerable)
176+ if ( currentIntroduced !== undefined ) {
177+ intervals . push ( { introduced : currentIntroduced } )
170178 }
179+
180+ return intervals
171181}
172182
173183/**
174184 * 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.
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.
176187 * @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
177188 */
178189function getFixedVersion (
@@ -187,18 +198,26 @@ function getFixedVersion(
187198 a => a . package . ecosystem === 'npm' && a . package . name === packageName ,
188199 )
189200
190- // Check each entry's ranges to find one that contains the current version
201+ // Check each entry's ranges to find the interval that contains the current version
191202 for ( const entry of packageAffectedEntries ) {
192203 if ( ! entry . ranges ) continue
193204
194205 for ( const range of entry . ranges ) {
195206 // Only handle SEMVER ranges (most common for npm)
196207 if ( range . type !== 'SEMVER' ) continue
197208
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
209+ const intervals = parseRangeIntervals ( range )
210+ for ( const interval of intervals ) {
211+ const introVersion = interval . introduced === '0' ? '0.0.0' : interval . introduced
212+ try {
213+ const afterIntro = semver . gte ( currentVersion , introVersion )
214+ const beforeFixed = ! interval . fixed || semver . lt ( currentVersion , interval . fixed )
215+ if ( afterIntro && beforeFixed && interval . fixed ) {
216+ return interval . fixed
217+ }
218+ } catch {
219+ continue
220+ }
202221 }
203222 }
204223 }
0 commit comments