[READ-ONLY] a fast, modern browser for the npm registry
at main 224 lines 8.4 kB view raw
1<script setup lang="ts"> 2import { SEVERITY_LEVELS } from '#shared/types' 3import { SEVERITY_COLORS } from '#shared/utils/severity' 4 5const props = defineProps<{ 6 packageName: string 7 version: string 8}>() 9 10const { data: vulnTree, status } = useDependencyAnalysis( 11 () => props.packageName, 12 () => props.version, 13) 14 15const isExpanded = shallowRef(false) 16const showAllPackages = shallowRef(false) 17const showAllVulnerabilities = shallowRef(false) 18 19const hasVulnerabilities = computed( 20 () => vulnTree.value && vulnTree.value.vulnerablePackages.length > 0, 21) 22 23// Banner - amber for better light mode contrast 24const bannerColor = 'border-amber-600/40 bg-amber-500/10 text-amber-800 dark:text-amber-400' 25 26const severityLabels = computed(() => ({ 27 critical: $t('package.vulnerabilities.severity.critical'), 28 high: $t('package.vulnerabilities.severity.high'), 29 moderate: $t('package.vulnerabilities.severity.moderate'), 30 low: $t('package.vulnerabilities.severity.low'), 31})) 32 33function getPackageSeverityLabel(severity: Exclude<OsvSeverityLevel, 'unknown'>) { 34 return severityLabels.value[severity] 35} 36 37const summaryText = computed(() => { 38 if (!vulnTree.value) return '' 39 const { totalCounts } = vulnTree.value 40 return SEVERITY_LEVELS.filter(s => totalCounts[s] > 0) 41 .map(s => `${totalCounts[s]} ${getPackageSeverityLabel(s)}`) 42 .join(', ') 43}) 44 45// Styling for each depth level - using accessible colors for both themes 46const depthStyles = { 47 root: { 48 bg: 'bg-amber-500/5 border-is-2 border-is-amber-600', 49 text: 'text-fg', 50 }, 51 direct: { 52 bg: 'bg-amber-500/5 border-is-2 border-is-amber-500', 53 text: 'text-fg-muted', 54 }, 55 transitive: { 56 bg: 'bg-amber-500/5 border-is-2 border-is-amber-400', 57 text: 'text-fg-muted', 58 }, 59} as const 60 61// Helper to get depth style with fallback 62function getDepthStyle(depth: string | undefined) { 63 if (depth && depth in depthStyles) { 64 return depthStyles[depth as keyof typeof depthStyles] 65 } 66 return depthStyles.transitive 67} 68</script> 69 70<template> 71 <section 72 v-if="status === 'success' && hasVulnerabilities" 73 aria-labelledby="vuln-tree-heading" 74 class="relative" 75 > 76 <!-- Collapsible vulnerability banner --> 77 <div role="alert" class="rounded-lg border overflow-hidden" :class="bannerColor"> 78 <!-- Header --> 79 <button 80 type="button" 81 class="w-full flex items-center justify-between gap-3 px-4 py-3 text-start transition-colors duration-200 hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/70" 82 :aria-expanded="isExpanded" 83 aria-controls="vuln-tree-details" 84 @click="isExpanded = !isExpanded" 85 > 86 <span class="flex items-center gap-2 min-w-0"> 87 <span class="i-lucide:circle-alert w-4 h-4 shrink-0" aria-hidden="true" /> 88 <span class="font-mono text-sm font-medium truncate"> 89 {{ 90 $t( 91 'package.vulnerabilities.tree_found', 92 { 93 vulns: vulnTree!.totalCounts.total, 94 packages: vulnTree!.vulnerablePackages.length, 95 total: vulnTree!.totalPackages, 96 }, 97 vulnTree!.totalCounts.total, 98 ) 99 }} 100 </span> 101 </span> 102 <span class="flex items-center gap-2 shrink-0"> 103 <span class="text-xs op-90 dark:op-80 hidden sm:inline">{{ summaryText }}</span> 104 <span 105 class="i-lucide:chevron-down w-4 h-4 transition-transform duration-200" 106 :class="{ 'rotate-180': isExpanded }" 107 aria-hidden="true" 108 /> 109 </span> 110 </button> 111 112 <!-- Expandable details --> 113 <div v-show="isExpanded" id="vuln-tree-details" class="border-t border-border bg-bg-subtle"> 114 <ul class="divide-y divide-border list-none m-0 p-0"> 115 <li 116 v-for="pkg in vulnTree!.vulnerablePackages.slice(0, showAllPackages ? undefined : 5)" 117 :key="`${pkg.name}@${pkg.version}`" 118 class="px-4 py-3" 119 :class="getDepthStyle(pkg.depth).bg" 120 > 121 <div class="flex items-center justify-between gap-2 mb-2"> 122 <div class="flex items-center gap-2 min-w-0 relative"> 123 <!-- Path badge - click to show tree popup --> 124 <DependencyPathPopup v-if="pkg.path && pkg.path.length > 1" :path="pkg.path" /> 125 126 <NuxtLink 127 :to="packageRoute(pkg.name, pkg.version)" 128 class="font-mono text-sm font-medium hover:underline truncate shrink min-w-0" 129 :class="getDepthStyle(pkg.depth).text" 130 > 131 {{ pkg.name }}@{{ pkg.version }} 132 </NuxtLink> 133 </div> 134 <div class="flex items-center gap-1 shrink-0"> 135 <span 136 v-for="s in SEVERITY_LEVELS.filter(s => pkg.counts[s] > 0)" 137 :key="s" 138 class="px-1.5 py-0.5 text-3xs font-mono rounded border" 139 :class="SEVERITY_COLORS[s]" 140 > 141 {{ pkg.counts[s] }} {{ getPackageSeverityLabel(s) }} 142 </span> 143 </div> 144 </div> 145 <!-- Show first 2 vulnerabilities --> 146 <ul class="space-y-1 list-none m-0 p-0"> 147 <li 148 v-for="vuln in pkg.vulnerabilities.slice(0, showAllVulnerabilities ? undefined : 2)" 149 :key="vuln.id" 150 class="flex items-center gap-2 text-xs text-fg-muted" 151 > 152 <a 153 :href="vuln.url" 154 target="_blank" 155 rel="noopener noreferrer" 156 class="font-mono hover:underline shrink-0" 157 > 158 {{ vuln.id }} 159 </a> 160 <span class="truncate w-0 flex-1">{{ vuln.summary }}</span> 161 <NuxtLink 162 v-if="vuln.fixedIn" 163 :to="packageRoute(pkg.name, vuln.fixedIn)" 164 class="shrink-0 font-mono text-emerald-600 dark:text-emerald-400 hover:underline" 165 :title="$t('package.vulnerabilities.fixed_in_title', { version: vuln.fixedIn })" 166 > 167 {{ vuln.fixedIn }} 168 </NuxtLink> 169 </li> 170 <li 171 v-if="pkg.vulnerabilities.length > 2 && !showAllVulnerabilities" 172 class="text-xs text-fg-subtle" 173 > 174 <button type="button" @click="showAllVulnerabilities = true"> 175 {{ 176 $t('package.vulnerabilities.more', { count: pkg.vulnerabilities.length - 2 }) 177 }} 178 </button> 179 </li> 180 </ul> 181 </li> 182 </ul> 183 184 <button 185 v-if="vulnTree!.vulnerablePackages.length > 5 && !showAllPackages" 186 type="button" 187 class="w-full px-4 py-2 text-xs font-mono text-fg-muted hover:text-fg border-t border-border transition-colors duration-200" 188 @click="showAllPackages = true" 189 > 190 {{ 191 $t('package.vulnerabilities.show_all_packages', { 192 count: vulnTree!.vulnerablePackages.length, 193 }) 194 }} 195 </button> 196 197 <!-- Warning if some queries failed --> 198 <div 199 v-if="vulnTree!.failedQueries" 200 class="px-4 py-2 text-xs text-fg-subtle border-t border-border flex items-center gap-2" 201 > 202 <span class="i-lucide:circle-alert w-3 h-3" aria-hidden="true" /> 203 <span>{{ $t('package.vulnerabilities.packages_failed', vulnTree!.failedQueries) }}</span> 204 </div> 205 </div> 206 </div> 207 </section> 208 209 <!-- Loading state - hidden (loading indicator shown in stats banner) --> 210 211 <!-- No vulnerabilities found - don't show anything (count is shown in stats banner) --> 212 213 <!-- Error state - subtle, not alarming --> 214 <section v-else-if="status === 'error'" aria-labelledby="vuln-tree-error"> 215 <div class="rounded-lg border border-border bg-bg-subtle px-4 py-3"> 216 <div class="flex items-center gap-2"> 217 <span class="i-lucide:circle-alert w-4 h-4 text-fg-subtle" aria-hidden="true" /> 218 <span class="text-sm text-fg-muted"> 219 {{ $t('package.vulnerabilities.scan_failed') }} 220 </span> 221 </div> 222 </div> 223 </section> 224</template>