UIPackage
Menu

Framework

Change language

Boilerplate repo

Vulnerability Scanner Report

blocksecurity

Snyk and Trivy style CVE vulnerability scanner report and dependency security audit with severity breakdown cards, CVSS metrics, package version diffs, advisory inspection drawer, and automated remediation PR actions.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/vulnerability-scanner-report.json
Named registry:npx shadcn-vue@latest add @uipkge/vulnerability-scanner-reportInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
targetRepostring'uipkge / core-registry:v2.4'optional
targetBranchstring'main (git:a4f891b)'optional
baseImagestring'node:22-alpine3.20'optional
lastScannedstring'Scanned 12m ago via Trivy v0.52'optional
gradestring'Grade B+'optional
gradeSummarystring'2 High CVEs'optional
totalDepsnumber1284optional
itemsCveItem[]optional
classHTMLAttributes['class']optional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

CveItem
interface CveItem {
  id: string
  title: string
  cwe: string
  severity: CveSeverity
  cvssScore: number
  cvssVector: string
  pkgName: string
  installedVersion: string
  fixedVersion: string
  exploitStatus: ExploitStatus
  exploitLabel: string
  attackVector: string
  attackComplexity: string
  privilegesRequired: string
  userInteraction: string
  description: string
  impact: string
  remediationDiff: {
    removed: string
    added: string
  }
  patchCommand: string
  advisoryUrl: string
}

Files installed (1)

  • app/components/blocks/VulnerabilityScannerReport.vue40.5 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import {
      AlertCircle,
      AlertTriangle,
      Check,
      CheckCircle2,
      Copy,
      Download,
      ExternalLink,
      FileCode2,
      GitPullRequest,
      Info,
      Package,
      RefreshCw,
      Search,
      ShieldAlert,
      ShieldCheck,
    } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent } from '@/components/ui/card'
    import { Input } from '@/components/ui/input'
    import { Progress } from '@/components/ui/progress'
    import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
    import {
      Sheet,
      SheetClose,
      SheetContent,
      SheetDescription,
      SheetFooter,
      SheetHeader,
      SheetTitle,
    } from '@/components/ui/sheet'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    
    export type CveSeverity = 'critical' | 'high' | 'medium' | 'low'
    export type ExploitStatus = 'poc' | 'none'
    
    export interface CveItem {
      id: string
      title: string
      cwe: string
      severity: CveSeverity
      cvssScore: number
      cvssVector: string
      pkgName: string
      installedVersion: string
      fixedVersion: string
      exploitStatus: ExploitStatus
      exploitLabel: string
      attackVector: string
      attackComplexity: string
      privilegesRequired: string
      userInteraction: string
      description: string
      impact: string
      remediationDiff: {
        removed: string
        added: string
      }
      patchCommand: string
      advisoryUrl: string
    }
    
    export interface VulnerabilityScannerReportProps {
      targetRepo?: string
      targetBranch?: string
      baseImage?: string
      lastScanned?: string
      grade?: string
      gradeSummary?: string
      totalDeps?: number
      items?: CveItem[]
      class?: HTMLAttributes['class']
    }
    
    const defaultCveItems: CveItem[] = [
      {
        id: 'CVE-2026-38419',
        title: 'Prototype Pollution via mergeCustomizer in lodash',
        cwe: 'CWE-1321',
        severity: 'high',
        cvssScore: 8.4,
        cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N',
        pkgName: 'lodash',
        installedVersion: '4.17.20',
        fixedVersion: '4.17.22+',
        exploitStatus: 'poc',
        exploitLabel: 'Proof of Concept available',
        attackVector: 'Network (Remote)',
        attackComplexity: 'Low',
        privilegesRequired: 'None',
        userInteraction: 'None',
        description:
          'A prototype pollution vulnerability in lodash prior to 4.17.22 allows unauthenticated remote attackers to modify Object.prototype properties via crafted JSON payloads, leading to denial of service or potential remote code execution.',
        impact: 'Critical data integrity failure and property tampering across server runtime objects.',
        remediationDiff: {
          removed: '  "lodash": "^4.17.20",',
          added: '  "lodash": "^4.17.22",',
        },
        patchCommand: 'npm install lodash@^4.17.22 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-38419',
      },
      {
        id: 'CVE-2026-29401',
        title: 'Server-Side Request Forgery (SSRF) bypass in axios redirect handler',
        cwe: 'CWE-918',
        severity: 'high',
        cvssScore: 7.8,
        cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N',
        pkgName: 'axios',
        installedVersion: '1.6.2',
        fixedVersion: '1.7.9+',
        exploitStatus: 'poc',
        exploitLabel: 'Proof of Concept available',
        attackVector: 'Network (Remote)',
        attackComplexity: 'Low',
        privilegesRequired: 'None',
        userInteraction: 'Required',
        description:
          'Axios versions <=1.6.8 improperly preserve sensitive Authorization and Cookie headers during cross-origin HTTP 302/307 redirects, exposing upstream credentials to untrusted third-party endpoints.',
        impact: 'Authorization bearer token leakage to arbitrary redirect targets.',
        remediationDiff: {
          removed: '  "axios": "^1.6.2",',
          added: '  "axios": "^1.7.9",',
        },
        patchCommand: 'npm install axios@^1.7.9 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-29401',
      },
      {
        id: 'CVE-2026-10492',
        title: 'Arbitrary File Overwrite via hardlink traversal in tar',
        cwe: 'CWE-59',
        severity: 'medium',
        cvssScore: 6.5,
        cvssVector: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H',
        pkgName: 'tar',
        installedVersion: '6.1.11',
        fixedVersion: '6.2.1+',
        exploitStatus: 'none',
        exploitLabel: 'No known exploit',
        attackVector: 'Local Archive Extraction',
        attackComplexity: 'Low',
        privilegesRequired: 'None',
        userInteraction: 'Required',
        description:
          'The tar package before 6.2.1 is vulnerable to arbitrary file write outside the extraction destination directory when untrusted tar archives contain hardlinks pointing to symlinked directories.',
        impact: 'Arbitrary filesystem overwrite upon unpacking unvetted tarball artifacts.',
        remediationDiff: {
          removed: '  "tar": "^6.1.11",',
          added: '  "tar": "^6.2.1",',
        },
        patchCommand: 'npm install tar@^6.2.1 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-10492',
      },
      {
        id: 'CVE-2026-84920',
        title: 'Signature verification bypass via algorithm confusion in jsonwebtoken',
        cwe: 'CWE-347',
        severity: 'medium',
        cvssScore: 6.1,
        cvssVector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N',
        pkgName: 'jsonwebtoken',
        installedVersion: '8.5.1',
        fixedVersion: '9.0.2+',
        exploitStatus: 'poc',
        exploitLabel: 'Proof of Concept available',
        attackVector: 'Network (Remote)',
        attackComplexity: 'High',
        privilegesRequired: 'None',
        userInteraction: 'None',
        description:
          'When jwt.verify() is executed without explicit algorithms whitelisting, attackers can forge unsigned tokens or switch asymmetric RS256 keys to symmetric HMAC evaluation with public certs.',
        impact: 'Unauthorized identity impersonation and claim forgery.',
        remediationDiff: {
          removed: '  "jsonwebtoken": "^8.5.1",',
          added: '  "jsonwebtoken": "^9.0.2",',
        },
        patchCommand: 'npm install jsonwebtoken@^9.0.2 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-84920',
      },
      {
        id: 'CVE-2026-18933',
        title: 'Regular Expression Denial of Service (ReDoS) in micromatch',
        cwe: 'CWE-1333',
        severity: 'medium',
        cvssScore: 5.3,
        cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L',
        pkgName: 'micromatch',
        installedVersion: '4.0.5',
        fixedVersion: '4.0.8+',
        exploitStatus: 'none',
        exploitLabel: 'No known exploit',
        attackVector: 'Network (Remote)',
        attackComplexity: 'Low',
        privilegesRequired: 'None',
        userInteraction: 'None',
        description:
          'A backtracking exponential regular expression in micromatch path globbing allows attackers sending crafted nested brace expressions to cause catastrophic CPU exhaustion and event loop freeze.',
        impact: 'Node.js main thread event-loop starvation and service unresponsiveness.',
        remediationDiff: {
          removed: '  "micromatch": "^4.0.5",',
          added: '  "micromatch": "^4.0.8",',
        },
        patchCommand: 'npm install micromatch@^4.0.8 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-18933',
      },
      {
        id: 'CVE-2026-04192',
        title: 'Information disclosure in child process error serialization in cross-spawn',
        cwe: 'CWE-209',
        severity: 'low',
        cvssScore: 3.7,
        cvssVector: 'CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N',
        pkgName: 'cross-spawn',
        installedVersion: '7.0.3',
        fixedVersion: '7.0.6+',
        exploitStatus: 'none',
        exploitLabel: 'No known exploit',
        attackVector: 'Local Environment',
        attackComplexity: 'High',
        privilegesRequired: 'Low',
        userInteraction: 'None',
        description:
          'Under verbose debug logging configurations, command environment variables containing auth tokens may be included in uncaught spawn error stacks printed to stdout.',
        impact: 'Accidental leakage of process environment credentials in CI runner logs.',
        remediationDiff: {
          removed: '  "cross-spawn": "^7.0.3",',
          added: '  "cross-spawn": "^7.0.6",',
        },
        patchCommand: 'npm install cross-spawn@^7.0.6 --save-exact',
        advisoryUrl: 'https://nvd.nist.gov/vuln/detail/CVE-2026-04192',
      },
    ]
    
    const props = withDefaults(defineProps<VulnerabilityScannerReportProps>(), {
      targetRepo: 'uipkge / core-registry:v2.4',
      targetBranch: 'main (git:a4f891b)',
      baseImage: 'node:22-alpine3.20',
      lastScanned: 'Scanned 12m ago via Trivy v0.52',
      grade: 'Grade B+',
      gradeSummary: '2 High CVEs',
      totalDeps: 1284,
    })
    
    const activeItems = computed(() => props.items ?? defaultCveItems)
    
    const searchQuery = ref('')
    const severityFilter = ref<string>('all')
    const exploitFilter = ref<string>('all')
    const isScanning = ref(false)
    const scanProgress = ref(100)
    const currentLastScanned = ref(props.lastScanned)
    const selectedCve = ref<CveItem | null>(null)
    const isDrawerOpen = ref(false)
    const copiedCveId = ref<string | null>(null)
    const fixedPrs = ref<Record<string, boolean>>({})
    const exportSuccess = ref(false)
    
    const summaryCounts = computed(() => {
      const all = activeItems.value
      return {
        critical: all.filter((i) => i.severity === 'critical').length,
        high: all.filter((i) => i.severity === 'high').length,
        medium: 5, // Display total detected across all direct/transitive trees
        low: 12, // Display total detected across all direct/transitive trees
        total: all.length,
      }
    })
    
    const filteredItems = computed(() => {
      const query = searchQuery.value.trim().toLowerCase()
      return activeItems.value.filter((item) => {
        const matchesSeverity = severityFilter.value === 'all' || item.severity === severityFilter.value
        const matchesExploit = exploitFilter.value === 'all' || item.exploitStatus === exploitFilter.value
        const matchesSearch =
          !query ||
          item.id.toLowerCase().includes(query) ||
          item.pkgName.toLowerCase().includes(query) ||
          item.title.toLowerCase().includes(query) ||
          item.cwe.toLowerCase().includes(query)
    
        return matchesSeverity && matchesExploit && matchesSearch
      })
    })
    
    function triggerRescan() {
      if (isScanning.value) return
      isScanning.value = true
      scanProgress.value = 15
    
      const interval = setInterval(() => {
        if (scanProgress.value < 90) {
          scanProgress.value += 25
        }
      }, 150)
    
      setTimeout(() => {
        clearInterval(interval)
        scanProgress.value = 100
        isScanning.value = false
        currentLastScanned.value = 'Scanned just now via Trivy v0.52'
      }, 800)
    }
    
    function openAdvisory(cve: CveItem) {
      selectedCve.value = cve
      isDrawerOpen.value = true
    }
    
    function resetFilters() {
      severityFilter.value = 'all'
      exploitFilter.value = 'all'
      searchQuery.value = ''
    }
    
    function handleAutoFix(cveId: string) {
      fixedPrs.value[cveId] = true
    }
    
    function copyCommand(cveId: string, command: string) {
      if (typeof navigator !== 'undefined' && navigator.clipboard) {
        navigator.clipboard.writeText(command)
        copiedCveId.value = cveId
        setTimeout(() => {
          if (copiedCveId.value === cveId) {
            copiedCveId.value = null
          }
        }, 2000)
      }
    }
    
    function exportSbom() {
      const sbomData = {
        spdxVersion: 'SPDX-2.3',
        dataLicense: 'CC0-1.0',
        SPDXID: 'SPDXRef-DOCUMENT',
        name: 'uipkge-core-registry-sbom',
        documentNamespace: 'https://uipkge.dev/spdxdocs/core-registry-v2.4',
        creationInfo: {
          created: new Date().toISOString(),
          creators: ['Tool: Trivy-v0.52', 'Organization: UIPKGE Security Squad'],
        },
        packages: (props.items || []).map((item) => ({
          name: item.pkgName,
          SPDXID: `SPDXRef-Package-${item.pkgName}`,
          versionInfo: item.installedVersion,
          downloadLocation: `https://registry.npmjs.org/${item.pkgName}/-/${item.pkgName}-${item.installedVersion}.tgz`,
          filesAnalyzed: false,
          securityVulnerabilities: [
            {
              id: item.id,
              severity: item.severity.toUpperCase(),
              cvssScore: item.cvssScore,
              remediation: item.fixedVersion,
            },
          ],
        })),
      }
    
      const jsonStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(sbomData, null, 2))
      if (typeof document !== 'undefined') {
        const downloadAnchor = document.createElement('a')
        downloadAnchor.setAttribute('href', jsonStr)
        downloadAnchor.setAttribute('download', `sbom-spdx-core-registry-${new Date().toISOString().slice(0, 10)}.json`)
        document.body.appendChild(downloadAnchor)
        downloadAnchor.click()
        downloadAnchor.remove()
    
        exportSuccess.value = true
        setTimeout(() => {
          exportSuccess.value = false
        }, 2000)
      }
    }
    
    function setSeverityFilter(sev: string) {
      if (severityFilter.value === sev) {
        severityFilter.value = 'all'
      } else {
        severityFilter.value = sev
      }
    }
    </script>
    
    <template>
      <div data-slot="vulnerability-scanner-report" :class="cn('w-full space-y-5', props.class)">
        <!-- Header Section -->
        <div
          class="border-border/80 bg-card text-card-foreground flex flex-col gap-4 rounded-xl border p-4 shadow-xs sm:p-5 lg:flex-row lg:items-center lg:justify-between"
        >
          <div class="space-y-1.5">
            <div class="flex flex-wrap items-center gap-2.5">
              <div
                class="bg-muted text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border"
              >
                <ShieldAlert class="text-warning size-4.5" />
              </div>
              <div>
                <div class="flex flex-wrap items-center gap-2">
                  <h1 class="text-foreground text-base font-semibold tracking-tight sm:text-lg">
                    {{ targetRepo }}
                  </h1>
                  <Badge variant="warning" class="gap-1 text-xs font-medium">
                    <span class="bg-warning size-1.5 rounded-full" />
                    {{ grade }} · {{ gradeSummary }}
                  </Badge>
                </div>
                <div class="text-muted-foreground flex flex-wrap items-center gap-2 pt-0.5 text-xs">
                  <span>{{ currentLastScanned }}</span>
                  <span class="opacity-40">·</span>
                  <span class="font-mono">{{ targetBranch }}</span>
                  <span class="opacity-40">·</span>
                  <span class="font-mono">{{ baseImage }}</span>
                  <span class="opacity-40">·</span>
                  <span>{{ totalDeps.toLocaleString() }} dependencies audited</span>
                </div>
              </div>
            </div>
          </div>
    
          <div class="flex flex-wrap items-center gap-2 self-start sm:gap-2.5 lg:self-center">
            <Button
              aria-label="Download attachment"
              variant="outline"
              size="sm"
              class="h-8 gap-1.5 text-xs font-medium shadow-xs"
              @click="exportSbom"
            >
              <Check v-if="exportSuccess" class="text-success size-3.5" />
              <Download v-else class="size-3.5" />
              <span>{{ exportSuccess ? 'SBOM Exported' : 'Export SBOM (SPDX)' }}</span>
            </Button>
    
            <Button
              variant="default"
              size="sm"
              class="h-8 gap-1.5 text-xs font-medium shadow-xs"
              :disabled="isScanning"
              @click="triggerRescan"
            >
              <RefreshCw :class="['size-3.5', isScanning ? 'animate-spin' : '']" />
              <span>{{ isScanning ? 'Scanning Container...' : 'Trigger Re-scan' }}</span>
            </Button>
          </div>
        </div>
    
        <!-- Active Scan Progress Track (visible during scan) -->
        <div v-if="isScanning" class="border-primary/20 bg-primary/5 space-y-1.5 rounded-lg border p-3">
          <div class="flex items-center justify-between gap-x-2 text-xs font-medium">
            <span class="text-primary flex items-center gap-1.5">
              <Search class="size-3.5" />
              Running AST & SBOM vulnerability correlation against NVD & OSV databases...
            </span>
            <span class="text-muted-foreground font-mono">{{ scanProgress }}%</span>
          </div>
          <Progress :model-value="scanProgress" class="h-1.5" />
        </div>
    
        <!-- 4 CVE Severity Summary Cards -->
        <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
          <!-- Critical Card -->
          <Card
            :class="[
              'border-border/80 bg-card text-card-foreground hover:border-border cursor-pointer shadow-xs transition-colors',
              severityFilter === 'critical' ? 'border-success/50 ring-success ring-2' : '',
            ]"
            @click="setSeverityFilter('critical')"
          >
            <CardContent class="p-4">
              <div class="flex items-start justify-between">
                <div class="space-y-1">
                  <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Critical Severity</p>
                  <div class="flex items-baseline gap-2">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight">
                      {{ summaryCounts.critical }}
                    </span>
                    <span class="text-success text-xs font-medium">0 CVEs</span>
                  </div>
                </div>
                <div
                  class="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-lg border"
                >
                  <ShieldCheck class="size-4.5" />
                </div>
              </div>
              <div
                class="border-border/50 text-muted-foreground mt-3 flex items-center justify-between gap-x-2 border-t pt-2 text-xs"
              >
                <span class="text-success font-medium">Zero critical exposures</span>
                <span class="font-mono text-xs">CVSS 9.0–10.0</span>
              </div>
            </CardContent>
          </Card>
    
          <!-- High Card -->
          <Card
            :class="[
              'border-border/80 bg-card text-card-foreground hover:border-border cursor-pointer shadow-xs transition-colors',
              severityFilter === 'high' ? 'border-destructive/50 ring-destructive ring-2' : '',
            ]"
            @click="setSeverityFilter('high')"
          >
            <CardContent class="p-4">
              <div class="flex items-start justify-between">
                <div class="space-y-1">
                  <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">High Severity</p>
                  <div class="flex items-baseline gap-2">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight">
                      {{ summaryCounts.high }}
                    </span>
                    <span class="text-destructive text-xs font-medium">2 CVEs</span>
                  </div>
                </div>
                <div
                  class="border-destructive/20 bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-lg border"
                >
                  <AlertTriangle class="size-4.5" />
                </div>
              </div>
              <div
                class="border-border/50 text-muted-foreground mt-3 flex items-center justify-between gap-x-2 border-t pt-2 text-xs"
              >
                <span class="text-destructive font-medium">Actionable patches ready</span>
                <span class="font-mono text-xs">CVSS 7.0–8.9</span>
              </div>
            </CardContent>
          </Card>
    
          <!-- Medium Card -->
          <Card
            :class="[
              'border-border/80 bg-card text-card-foreground hover:border-border cursor-pointer shadow-xs transition-colors',
              severityFilter === 'medium' ? 'border-warning/50 ring-warning ring-2' : '',
            ]"
            @click="setSeverityFilter('medium')"
          >
            <CardContent class="p-4">
              <div class="flex items-start justify-between">
                <div class="space-y-1">
                  <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Medium Severity</p>
                  <div class="flex items-baseline gap-2">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight">
                      {{ summaryCounts.medium }}
                    </span>
                    <span class="text-warning text-xs font-medium">5 CVEs</span>
                  </div>
                </div>
                <div
                  class="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-lg border"
                >
                  <AlertCircle class="size-4.5" />
                </div>
              </div>
              <div
                class="border-border/50 text-muted-foreground mt-3 flex items-center justify-between gap-x-2 border-t pt-2 text-xs"
              >
                <span class="text-warning font-medium">3 in direct dependencies</span>
                <span class="font-mono text-xs">CVSS 4.0–6.9</span>
              </div>
            </CardContent>
          </Card>
    
          <!-- Low Card -->
          <Card
            :class="[
              'border-border/80 bg-card text-card-foreground hover:border-border cursor-pointer shadow-xs transition-colors',
              severityFilter === 'low' ? 'border-info/50 ring-info ring-2' : '',
            ]"
            @click="setSeverityFilter('low')"
          >
            <CardContent class="p-4">
              <div class="flex items-start justify-between">
                <div class="space-y-1">
                  <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Low Severity</p>
                  <div class="flex items-baseline gap-2">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight">
                      {{ summaryCounts.low }}
                    </span>
                    <span class="text-info text-xs font-medium">12 CVEs</span>
                  </div>
                </div>
                <div class="border-info/20 bg-info/10 text-info flex size-8 items-center justify-center rounded-lg border">
                  <Info class="size-4.5" />
                </div>
              </div>
              <div
                class="border-border/50 text-muted-foreground mt-3 flex items-center justify-between gap-x-2 border-t pt-2 text-xs"
              >
                <span class="text-info font-medium">Informational & debug</span>
                <span class="font-mono text-xs">CVSS 0.1–3.9</span>
              </div>
            </CardContent>
          </Card>
        </div>
    
        <!-- Filter & Search Toolbar -->
        <div class="flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between">
          <div class="flex flex-1 flex-wrap items-center gap-2">
            <div class="relative w-full sm:w-72">
              <Search
                class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
              />
              <Input v-model="searchQuery" placeholder="Search CVE, package, or CWE..." class="h-8 pl-8 text-xs" />
            </div>
    
            <!-- Severity Filter Select -->
            <Select v-model="severityFilter">
              <SelectTrigger class="h-8 w-36 text-xs capitalize">
                <SelectValue placeholder="All Severities" />
              </SelectTrigger>
              <SelectContent align="start">
                <SelectItem value="all">All Severities</SelectItem>
                <SelectItem value="critical">Critical (0)</SelectItem>
                <SelectItem value="high">High (2)</SelectItem>
                <SelectItem value="medium">Medium (5)</SelectItem>
                <SelectItem value="low">Low (12)</SelectItem>
              </SelectContent>
            </Select>
    
            <!-- Exploitability Filter Select -->
            <Select v-model="exploitFilter">
              <SelectTrigger class="h-8 w-44 text-xs">
                <SelectValue placeholder="All Exploit Status" />
              </SelectTrigger>
              <SelectContent align="start">
                <SelectItem value="all">All Exploit Status</SelectItem>
                <SelectItem value="poc">PoC Available</SelectItem>
                <SelectItem value="none">No Known Exploit</SelectItem>
              </SelectContent>
            </Select>
    
            <Button
              v-if="severityFilter !== 'all' || exploitFilter !== 'all' || searchQuery"
              variant="ghost"
              size="sm"
              class="text-muted-foreground hover:text-foreground h-8 px-2 text-xs"
              @click="resetFilters"
            >
              Reset filters
            </Button>
          </div>
    
          <div class="text-muted-foreground flex items-center gap-2 text-xs">
            <span
              >Showing <strong class="text-foreground font-mono">{{ filteredItems.length }}</strong> of
              {{ activeItems.length }} audited vulnerabilities</span
            >
          </div>
        </div>
    
        <!-- Vulnerability Table Card -->
        <Card class="border-border/80 bg-card text-card-foreground overflow-hidden shadow-xs">
          <div class="overflow-x-auto">
            <Table>
              <TableHeader>
                <TableRow class="hover:bg-transparent">
                  <TableHead class="min-w-[220px]">Vulnerability / CVE</TableHead>
                  <TableHead class="min-w-[130px]">Severity</TableHead>
                  <TableHead class="min-w-[160px]">Affected Package</TableHead>
                  <TableHead class="min-w-[170px]">Remediation Fixed</TableHead>
                  <TableHead class="min-w-[180px]">Exploitability</TableHead>
                  <TableHead class="min-w-[180px] text-right">Actions</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                <TableRow
                  v-for="item in filteredItems"
                  :key="item.id"
                  class="hover:bg-muted/40 cursor-pointer transition-colors"
                  @click="openAdvisory(item)"
                >
                  <!-- Vulnerability / CVE -->
                  <TableCell class="py-3 align-top">
                    <div class="space-y-1">
                      <div class="flex items-center gap-2">
                        <span class="text-foreground font-mono text-xs font-semibold hover:underline">
                          {{ item.id }}
                        </span>
                        <Badge variant="outline" class="px-1.5 py-0 font-mono text-xs">
                          {{ item.cwe }}
                        </Badge>
                      </div>
                      <p class="text-muted-foreground line-clamp-1 max-w-[220px] text-xs">
                        {{ item.title }}
                      </p>
                    </div>
                  </TableCell>
    
                  <!-- Severity & CVSS Score -->
                  <TableCell class="py-3 align-top">
                    <div class="space-y-1">
                      <Badge
                        v-if="item.severity === 'critical'"
                        variant="destructive"
                        class="gap-1 font-mono text-xs font-medium"
                      >
                        <AlertCircle class="size-3" />
                        CVSS {{ item.cvssScore }} Critical
                      </Badge>
                      <Badge
                        v-else-if="item.severity === 'high'"
                        class="border-destructive/20 bg-destructive/10 text-destructive gap-1 font-mono text-xs font-medium"
                      >
                        <AlertTriangle class="size-3" />
                        CVSS {{ item.cvssScore }} High
                      </Badge>
                      <Badge
                        v-else-if="item.severity === 'medium'"
                        variant="warning"
                        class="gap-1 font-mono text-xs font-medium"
                      >
                        <AlertCircle class="size-3" />
                        CVSS {{ item.cvssScore }} Med
                      </Badge>
                      <Badge v-else variant="info" class="gap-1 font-mono text-xs font-medium">
                        <Info class="size-3" />
                        CVSS {{ item.cvssScore }} Low
                      </Badge>
                      <p class="text-muted-foreground font-mono text-xs">v3.1 Vector</p>
                    </div>
                  </TableCell>
    
                  <!-- Affected Package & Version -->
                  <TableCell class="py-3 align-top">
                    <div class="flex items-center gap-2">
                      <Package class="text-muted-foreground size-3.5 shrink-0" />
                      <div class="min-w-0">
                        <p class="text-foreground truncate font-mono text-xs font-medium">
                          {{ item.pkgName }}
                        </p>
                        <p class="text-muted-foreground font-mono text-xs">v{{ item.installedVersion }}</p>
                      </div>
                    </div>
                  </TableCell>
    
                  <!-- Remediation Fixed Version -->
                  <TableCell class="py-3 align-top">
                    <div class="space-y-1">
                      <div
                        class="border-success/20 bg-success/10 text-success inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs font-medium"
                      >
                        <CheckCircle2 class="text-success size-3" />
                        {{ item.fixedVersion }}
                      </div>
                      <p class="text-muted-foreground text-xs">Upgrade ready</p>
                    </div>
                  </TableCell>
    
                  <!-- Exploitability Status -->
                  <TableCell class="py-3 align-top">
                    <div class="flex items-center gap-1.5">
                      <span
                        :class="[
                          'size-2 shrink-0 rounded-full',
                          item.exploitStatus === 'poc' ? 'bg-warning animate-pulse' : 'bg-muted-foreground/50',
                        ]"
                      />
                      <span
                        :class="[
                          'text-xs font-medium',
                          item.exploitStatus === 'poc' ? 'text-warning' : 'text-muted-foreground',
                        ]"
                      >
                        {{ item.exploitLabel }}
                      </span>
                    </div>
                  </TableCell>
    
                  <!-- Row Action Buttons -->
                  <TableCell class="py-3 text-right align-top" @click.stop>
                    <div class="flex items-center justify-end gap-1.5">
                      <Button
                        variant="ghost"
                        size="sm"
                        class="text-muted-foreground hover:text-foreground h-7 px-2.5 text-xs"
                        @click="openAdvisory(item)"
                      >
                        View Advisory
                      </Button>
    
                      <Button
                        v-if="!fixedPrs[item.id]"
                        variant="outline"
                        size="sm"
                        class="h-7 gap-1 px-2.5 text-xs font-medium shadow-xs"
                        @click="handleAutoFix(item.id)"
                      >
                        <GitPullRequest class="size-3" />
                        Auto-Fix PR
                      </Button>
                      <Badge v-else variant="success" class="h-7 gap-1 px-2.5 text-xs font-medium">
                        <Check class="size-3" />
                        PR #849 Opened
                      </Badge>
                    </div>
                  </TableCell>
                </TableRow>
    
                <TableRow v-if="filteredItems.length === 0">
                  <TableCell colspan="6" class="h-32 text-center">
                    <div class="flex flex-col items-center justify-center gap-1.5">
                      <ShieldCheck class="text-success size-7" />
                      <p class="text-foreground text-sm font-medium">No matching vulnerabilities found</p>
                      <p class="text-muted-foreground text-xs">All packages satisfy the active filter constraints.</p>
                    </div>
                  </TableCell>
                </TableRow>
              </TableBody>
            </Table>
          </div>
    
          <!-- Table Footer Metadata -->
          <div
            class="border-border/60 bg-muted/20 text-muted-foreground flex flex-col gap-2 border-t px-4 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between"
          >
            <div class="flex items-center gap-3">
              <span class="flex items-center gap-1">
                <span class="bg-success size-2 rounded-full" />
                NVD Sync Live
              </span>
              <span class="opacity-40">·</span>
              <span>CVSS Score Engine: v3.1 Specification</span>
            </div>
            <div class="flex items-center gap-2 font-mono">
              <span>Target hash: 9a20bf183</span>
            </div>
          </div>
        </Card>
    
        <!-- Detailed CVE Advisory Drawer / Sheet -->
        <Sheet v-model:open="isDrawerOpen">
          <SheetContent class="w-full space-y-6 overflow-y-auto p-6 sm:max-w-xl md:max-w-2xl">
            <SheetHeader v-if="selectedCve" class="space-y-2 p-0 text-left">
              <div class="flex items-center justify-between gap-3 pr-6">
                <div class="flex items-center gap-2">
                  <Badge
                    v-if="selectedCve.severity === 'critical'"
                    variant="destructive"
                    class="font-mono text-xs font-semibold"
                  >
                    CRITICAL {{ selectedCve.cvssScore }}
                  </Badge>
                  <Badge
                    v-else-if="selectedCve.severity === 'high'"
                    class="border-destructive/20 bg-destructive/10 text-destructive font-mono text-xs font-semibold"
                  >
                    HIGH {{ selectedCve.cvssScore }}
                  </Badge>
                  <Badge
                    v-else-if="selectedCve.severity === 'medium'"
                    variant="warning"
                    class="font-mono text-xs font-semibold"
                  >
                    MEDIUM {{ selectedCve.cvssScore }}
                  </Badge>
                  <Badge v-else variant="info" class="font-mono text-xs font-semibold">
                    LOW {{ selectedCve.cvssScore }}
                  </Badge>
    
                  <Badge variant="outline" class="font-mono text-xs">
                    {{ selectedCve.cwe }}
                  </Badge>
                </div>
    
                <a
                  :href="selectedCve.advisoryUrl"
                  target="_blank"
                  rel="noopener noreferrer"
                  class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs"
                >
                  <span>NVD Source</span>
                  <ExternalLink class="size-3" />
                </a>
              </div>
    
              <SheetTitle class="text-lg font-bold tracking-tight">
                {{ selectedCve.id }}
              </SheetTitle>
              <SheetDescription class="text-foreground/80 text-xs font-medium">
                {{ selectedCve.title }}
              </SheetDescription>
            </SheetHeader>
    
            <div v-if="selectedCve" class="space-y-5 text-xs">
              <!-- CVSS 3.1 Metrics Grid -->
              <div class="border-border/80 bg-muted/30 space-y-3 rounded-lg border p-3.5">
                <div class="flex items-center justify-between gap-x-2">
                  <span class="text-muted-foreground font-medium">CVSS v3.1 Base Score</span>
                  <span class="text-foreground font-mono text-sm font-bold">{{ selectedCve.cvssScore }} / 10.0</span>
                </div>
                <Progress :model-value="selectedCve.cvssScore * 10" class="h-2" />
    
                <div class="grid grid-cols-2 gap-2.5 pt-1 text-xs sm:grid-cols-4">
                  <div class="bg-card border-border/60 rounded-md border p-2">
                    <p class="text-muted-foreground text-xs">Attack Vector</p>
                    <p class="text-foreground mt-0.5 truncate font-medium">{{ selectedCve.attackVector }}</p>
                  </div>
                  <div class="bg-card border-border/60 rounded-md border p-2">
                    <p class="text-muted-foreground text-xs">Complexity</p>
                    <p class="text-foreground mt-0.5 font-medium">{{ selectedCve.attackComplexity }}</p>
                  </div>
                  <div class="bg-card border-border/60 rounded-md border p-2">
                    <p class="text-muted-foreground text-xs">Privileges</p>
                    <p class="text-foreground mt-0.5 font-medium">{{ selectedCve.privilegesRequired }}</p>
                  </div>
                  <div class="bg-card border-border/60 rounded-md border p-2">
                    <p class="text-muted-foreground text-xs">Interaction</p>
                    <p class="text-foreground mt-0.5 font-medium">{{ selectedCve.userInteraction }}</p>
                  </div>
                </div>
    
                <!-- CVSS Vector String -->
                <div
                  class="border-border/50 text-muted-foreground flex items-center justify-between gap-x-2 border-t pt-2 font-mono text-xs"
                >
                  <span class="truncate">{{ selectedCve.cvssVector }}</span>
                </div>
              </div>
    
              <!-- Description & Technical Impact -->
              <div class="space-y-2">
                <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Vulnerability Overview</h3>
                <p class="text-foreground text-xs leading-relaxed">
                  {{ selectedCve.description }}
                </p>
              </div>
    
              <div class="space-y-2">
                <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Technical Impact</h3>
                <div class="bg-card border-border/80 text-foreground/90 rounded-md border p-3 text-xs">
                  {{ selectedCve.impact }}
                </div>
              </div>
    
              <!-- Package Path & Version Comparison -->
              <div class="space-y-2">
                <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Dependency State</h3>
                <div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
                  <div class="border-destructive/30 bg-destructive/5 rounded-md border p-3">
                    <p class="text-destructive text-xs font-medium">Installed Version</p>
                    <p class="text-foreground mt-0.5 font-mono font-bold">
                      {{ selectedCve.pkgName }}@{{ selectedCve.installedVersion }}
                    </p>
                    <p class="text-muted-foreground mt-1 text-xs">Direct dependency in package.json</p>
                  </div>
    
                  <div class="border-success/30 bg-success/5 rounded-md border p-3">
                    <p class="text-success text-xs font-medium">Remediation Target</p>
                    <p class="text-foreground mt-0.5 font-mono font-bold">
                      {{ selectedCve.pkgName }}@{{ selectedCve.fixedVersion }}
                    </p>
                    <p class="text-muted-foreground mt-1 text-xs">Non-breaking patch update</p>
                  </div>
                </div>
              </div>
    
              <!-- Remediation Code Diff -->
              <div class="space-y-2">
                <div class="flex items-center justify-between gap-x-2">
                  <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    package.json Patch Diff
                  </h3>
                  <span class="text-muted-foreground font-mono text-xs">package.json</span>
                </div>
                <div class="overflow-hidden rounded-md border bg-zinc-950 font-mono text-xs text-zinc-100 shadow-inner">
                  <div
                    class="text-muted-foreground flex items-center gap-2 border-b border-zinc-800 bg-zinc-900/90 px-3 py-1.5 text-xs"
                  >
                    <FileCode2 class="size-3.5" />
                    <span>dependencies</span>
                  </div>
                  <div class="space-y-1 p-3 text-xs leading-relaxed select-text">
                    <div class="bg-destructive/15 text-destructive rounded-xs px-1.5 py-0.5">
                      - {{ selectedCve.remediationDiff.removed }}
                    </div>
                    <div class="bg-success/15 text-success rounded-xs px-1.5 py-0.5">
                      + {{ selectedCve.remediationDiff.added }}
                    </div>
                  </div>
                </div>
              </div>
    
              <!-- Remediation CLI Command Box -->
              <div class="space-y-2">
                <div class="flex items-center justify-between gap-x-2">
                  <h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    CLI Remediation Command
                  </h3>
                  <Button
                    variant="ghost"
                    size="xs"
                    class="h-6 gap-1 px-2 text-xs"
                    @click="copyCommand(selectedCve.id, selectedCve.patchCommand)"
                  >
                    <Check v-if="copiedCveId === selectedCve.id" class="text-success size-3" />
                    <Copy v-else class="size-3" />
                    <span>{{ copiedCveId === selectedCve.id ? 'Copied' : 'Copy command' }}</span>
                  </Button>
                </div>
                <div
                  class="border-border bg-muted/60 flex items-center justify-between gap-x-2 rounded-md border px-3 py-2 font-mono text-xs"
                >
                  <span class="text-foreground truncate">{{ selectedCve.patchCommand }}</span>
                </div>
              </div>
            </div>
    
            <SheetFooter
              v-if="selectedCve"
              class="border-border/80 flex flex-row items-center justify-end gap-2 border-t pt-4"
            >
              <SheetClose as-child>
                <Button variant="outline" size="sm" class="text-xs"> Close </Button>
              </SheetClose>
    
              <Button
                v-if="!fixedPrs[selectedCve.id]"
                variant="default"
                size="sm"
                class="gap-1.5 text-xs font-medium"
                @click="handleAutoFix(selectedCve.id)"
              >
                <GitPullRequest class="size-3.5" />
                Open Auto-Fix Pull Request
              </Button>
              <Badge v-else variant="success" class="h-8 gap-1.5 px-3 text-xs font-medium">
                <Check class="size-3.5" />
                PR #849 Created & Assigned
              </Badge>
            </SheetFooter>
          </SheetContent>
        </Sheet>
      </div>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/vulnerability-scanner-report.json