Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ function getDeprecatedDepInfo(depName: string) {
return vulnTree.value.deprecatedPackages.find(p => p.name === depName && p.depth === 'direct')
}

// Cache URL dependency lookups with computed map
const urlDepMap = computed(() => {
if (!vulnTree.value) return new Map()
return new Map(vulnTree.value.urlDependencies.map(dep => [dep.name, dep]))
})

// Check if a dependency uses git: or https: URL
function getUrlDepInfo(depName: string) {
return urlDepMap.value.get(depName) ?? null
}

// Expanded state for each section
const depsExpanded = shallowRef(false)
const peerDepsExpanded = shallowRef(false)
Expand Down Expand Up @@ -73,6 +84,8 @@ const sortedOptionalDependencies = computed(() => {

// Get version tooltip
function getDepVersionTooltip(dep: string, version: string) {
const urlDep = getUrlDepInfo(dep)
if (urlDep) return urlDep.url
const outdated = outdatedDeps.value[dep]
if (outdated) return getOutdatedTooltip(outdated, t)
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return version
Expand All @@ -82,6 +95,7 @@ function getDepVersionTooltip(dep: string, version: string) {

// Get version class
function getDepVersionClass(dep: string) {
if (getUrlDepInfo(dep)) return 'text-orange-700 dark:text-orange-500'
const outdated = outdatedDeps.value[dep]
if (outdated) return getVersionClass(outdated)
if (getVulnerableDepInfo(dep) || getDeprecatedDepInfo(dep)) return getVersionClass(undefined)
Expand Down Expand Up @@ -164,6 +178,19 @@ const numberFormatter = useNumberFormatter()
>
<span class="sr-only">{{ $t('package.deprecated.label') }}</span>
</LinkBase>
<TooltipApp
v-if="getUrlDepInfo(dep)"
class="shrink-0 text-orange-700 dark:text-orange-500"
:text="getUrlDepInfo(dep)!.url"
>
<button
type="button"
class="p-2 -m-2"
:aria-label="`git/https dependency: ${getUrlDepInfo(dep)!.url}`"
>
<span class="i-lucide:triangle-alert w-3 h-3" aria-hidden="true" />
</button>
</TooltipApp>
<LinkBase
:to="packageRoute(dep, version)"
class="block truncate"
Expand Down
75 changes: 74 additions & 1 deletion server/utils/dependency-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
PackageVulnerabilityInfo,
VulnerabilityTreeResult,
DeprecatedPackageInfo,
UrlDependencyInfo,
OsvAffected,
OsvRange,
} from '#shared/types/dependency-analysis'
Expand Down Expand Up @@ -255,6 +256,69 @@ function getSeverityLevel(vuln: OsvVulnerability): OsvSeverityLevel {
return 'unknown'
}

/**
* Check if a dependency URL is a git: or https: URL that should be flagged.
*/
function isUrlDependency(url: string): boolean {
return (
url.startsWith('git:') ||
url.startsWith('git+') ||
url.startsWith('http:') ||
url.startsWith('https:') ||
url.startsWith('file:')
)
}

/**
* Scan a package's dependencies for git: and https: URLs.
* Returns a map of package names to their URL dependencies.
*/
async function scanUrlDependencies(
name: string,
version: string,
depth: DependencyDepth,
path: string[],
): Promise<UrlDependencyInfo[]> {
try {
const packument = await fetchNpmPackage(name)
const versionData = packument.versions[version]
if (!versionData) return []

const urlDeps: UrlDependencyInfo[] = []
// Include devDependencies only for the root package
const allDeps = depth === 'root'
? {
...versionData.dependencies,
...versionData.optionalDependencies,
...versionData.devDependencies,
}
: {
...versionData.dependencies,
...versionData.optionalDependencies,
}

// URL dependencies are children of the current package, so their depth is one level deeper
const dependencyDepth: DependencyDepth = depth === 'root' ? 'direct' : 'transitive'

for (const [depName, depUrl] of Object.entries(allDeps || {})) {
if (isUrlDependency(depUrl)) {
urlDeps.push({
name: depName,
url: depUrl,
depth: dependencyDepth,
path: [...path, `${depName}@${depUrl}`],
})
}
}

return urlDeps
} catch (error) {
// oxlint-disable-next-line no-console -- log URL dependency scan failures for debugging
console.warn(`[dep-analysis] URL dependency scan failed for ${name}@${version}:`, error)
return []
}
}

/**
* Analyze entire dependency tree for vulnerabilities and deprecated packages.
* Uses OSV batch API for efficient vulnerability discovery, then fetches
Expand Down Expand Up @@ -289,6 +353,14 @@ export const analyzeDependencyTree = defineCachedFunction(
return depthOrder[a.depth] - depthOrder[b.depth]
})

// Scan for git: and https: URL dependencies in all packages
const urlDepResults = await mapWithConcurrency(
packages,
pkg => scanUrlDependencies(pkg.name, pkg.version, pkg.depth, pkg.path),
OSV_DETAIL_CONCURRENCY,
)
const urlDependencies = urlDepResults.flat()

// Step 1: Use batch API to find which packages have vulnerabilities
// This is much faster than individual queries - one request for all packages
const { vulnerableIndices, failed: batchFailed } = await queryOsvBatch(packages)
Expand Down Expand Up @@ -347,6 +419,7 @@ export const analyzeDependencyTree = defineCachedFunction(
version,
vulnerablePackages,
deprecatedPackages,
urlDependencies,
totalPackages: packages.length,
failedQueries,
totalCounts,
Expand All @@ -356,6 +429,6 @@ export const analyzeDependencyTree = defineCachedFunction(
maxAge: 60 * 60,
swr: true,
name: 'dependency-analysis',
getKey: (name: string, version: string) => `v2:${name}@${version}`,
getKey: (name: string, version: string) => `v3:${name}@${version}`,
},
)
15 changes: 15 additions & 0 deletions shared/types/dependency-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@ export interface DeprecatedPackageInfo {
message: string
}

/**
* URL-based dependency info (git:, https:)
*/
export interface UrlDependencyInfo {
name: string
/** The git: or https: URL */
url: string
/** Depth in dependency tree: root (0), direct (1), transitive (2+) */
depth: DependencyDepth
/** Dependency path from root package */
path: string[]
}

/**
* Result of dependency tree analysis
*/
Expand All @@ -192,6 +205,8 @@ export interface VulnerabilityTreeResult {
vulnerablePackages: PackageVulnerabilityInfo[]
/** All deprecated packages in the tree */
deprecatedPackages: DeprecatedPackageInfo[]
/** All dependencies using git: or https: URLs */
urlDependencies: UrlDependencyInfo[]
/** Total packages analyzed */
totalPackages: number
/** Number of packages that could not be checked (OSV query failed) */
Expand Down
Loading