Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/components/Package/VulnerabilityTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ function getDepthStyle(depth: string | undefined) {
{{ vuln.id }}
</a>
<span class="truncate w-0 flex-1">{{ vuln.summary }}</span>
<NuxtLink
v-if="vuln.fixedIn"
:to="packageRoute(pkg.name, vuln.fixedIn)"
class="shrink-0 font-mono text-emerald-600 dark:text-emerald-400 hover:underline"
:title="$t('package.vulnerabilities.fixed_in_title', { version: vuln.fixedIn })"
>
→ {{ vuln.fixedIn }}
</NuxtLink>
</li>
<li
v-if="pkg.vulnerabilities.length > 2 && !showAllVulnerabilities"
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@
"high": "Hoch",
"moderate": "Mittel",
"low": "Niedrig"
}
},
"fixed_in_title": "Behoben in Version {version}"
},
"deprecated": {
"label": "Veraltet",
Expand Down
3 changes: 2 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@
"high": "high",
"moderate": "moderate",
"low": "low"
}
},
"fixed_in_title": "Fixed in version {version}"
},
"deprecated": {
"label": "Deprecated",
Expand Down
3 changes: 2 additions & 1 deletion lunaria/files/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@
"high": "Hoch",
"moderate": "Mittel",
"low": "Niedrig"
}
},
"fixed_in_title": "Behoben in Version {version}"
},
"deprecated": {
"label": "Veraltet",
Expand Down
3 changes: 2 additions & 1 deletion lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@
"high": "high",
"moderate": "moderate",
"low": "low"
}
},
"fixed_in_title": "Fixed in version {version}"
},
"deprecated": {
"label": "Deprecated",
Expand Down
3 changes: 2 additions & 1 deletion lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@
"high": "high",
"moderate": "moderate",
"low": "low"
}
},
"fixed_in_title": "Fixed in version {version}"
},
"deprecated": {
"label": "Deprecated",
Expand Down
81 changes: 81 additions & 0 deletions server/utils/dependency-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import type {
PackageVulnerabilityInfo,
VulnerabilityTreeResult,
DeprecatedPackageInfo,
OsvAffected,
OsvRange,
} from '#shared/types/dependency-analysis'
import { mapWithConcurrency } from '#shared/utils/async'
import { resolveDependencyTree } from './dependency-resolver'
import * as semver from 'semver'

/** Maximum concurrent requests for fetching vulnerability details */
const OSV_DETAIL_CONCURRENCY = 25
Expand Down Expand Up @@ -115,6 +118,7 @@ async function queryOsvDetails(pkg: PackageQueryInfo): Promise<PackageVulnerabil
severity,
aliases: vuln.aliases || [],
url: getVulnerabilityUrl(vuln),
fixedIn: getFixedVersion(vuln.affected, pkg.name, pkg.version),
})
}

Expand Down Expand Up @@ -144,6 +148,83 @@ function getVulnerabilityUrl(vuln: OsvVulnerability): string {
return `https://osv.dev/vulnerability/${vuln.id}`
}

/**
* Parse OSV range events into introduced/fixed pairs.
* OSV events form a timeline: [introduced, fixed, introduced, fixed, ...]
* A single range can have multiple introduced/fixed pairs representing
* periods where the vulnerability was active, was fixed, and was reintroduced.
* @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
*/
function parseRangeIntervals(range: OsvRange): Array<{ introduced: string; fixed?: string }> {
const intervals: Array<{ introduced: string; fixed?: string }> = []
let currentIntroduced: string | undefined

for (const event of range.events) {
if (event.introduced !== undefined) {
// Start a new interval (close previous open one if any)
if (currentIntroduced !== undefined) {
intervals.push({ introduced: currentIntroduced })
}
currentIntroduced = event.introduced
} else if (event.fixed !== undefined && currentIntroduced !== undefined) {
intervals.push({ introduced: currentIntroduced, fixed: event.fixed })
currentIntroduced = undefined
}
}

// Handle trailing introduced with no fixed (still vulnerable)
if (currentIntroduced !== undefined) {
intervals.push({ introduced: currentIntroduced })
}

return intervals
}

/**
* Extract the fixed version for a specific package version from vulnerability data.
* Finds the interval that contains the current version and returns its fixed version
* Finds the interval that contains the current version and returns its fixed version.
* @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
*/
function getFixedVersion(
affected: OsvAffected[] | undefined,
packageName: string,
currentVersion: string,
): string | undefined {
if (!affected) return undefined

// Find all affected entries for this specific package
const packageAffectedEntries = affected.filter(
a => a.package.ecosystem === 'npm' && a.package.name === packageName,
)

// Check each entry's ranges to find the interval that contains the current version
for (const entry of packageAffectedEntries) {
if (!entry.ranges) continue

for (const range of entry.ranges) {
// Only handle SEMVER ranges (most common for npm)
if (range.type !== 'SEMVER') continue

const intervals = parseRangeIntervals(range)
for (const interval of intervals) {
const introVersion = interval.introduced === '0' ? '0.0.0' : interval.introduced
try {
const afterIntro = semver.gte(currentVersion, introVersion)
const beforeFixed = !interval.fixed || semver.lt(currentVersion, interval.fixed)
if (afterIntro && beforeFixed && interval.fixed) {
return interval.fixed
}
} catch {
continue
}
}
}
}

return undefined
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getSeverityLevel(vuln: OsvVulnerability): OsvSeverityLevel {
const dbSeverity = vuln.database_specific?.severity?.toLowerCase()
if (dbSeverity) {
Expand Down
34 changes: 34 additions & 0 deletions shared/types/dependency-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,37 @@ export interface OsvReference {
url: string
}

/**
* Version range event from OSV affected data
* @see https://ossf.github.io/osv-schema/#affectedrangesevents-fields
*/
export interface OsvRangeEvent {
introduced?: string
fixed?: string
last_affected?: string
limit?: string
}

/**
* Version range from OSV affected data
*/
export interface OsvRange {
type: 'SEMVER' | 'ECOSYSTEM' | 'GIT'
events: OsvRangeEvent[]
}

/**
* Affected package info from OSV
*/
export interface OsvAffected {
package: {
ecosystem: string
name: string
}
ranges?: OsvRange[]
versions?: string[]
}

/**
* Individual vulnerability record from OSV
*/
Expand All @@ -48,6 +79,7 @@ export interface OsvVulnerability {
published?: string
severity?: OsvSeverity[]
references?: OsvReference[]
affected?: OsvAffected[]
database_specific?: {
severity?: string
cwe_ids?: string[]
Expand Down Expand Up @@ -97,6 +129,8 @@ export interface VulnerabilitySummary {
severity: OsvSeverityLevel
aliases: string[]
url: string
/** Version that fixes this vulnerability (if known) */
fixedIn?: string
}

/**
Expand Down
Loading
Loading