From 1098929a711f03b0056033da5ef7b7a1c8578b07 Mon Sep 17 00:00:00 2001 From: Jordan Jensen Date: Fri, 27 Jun 2025 12:20:29 -0700 Subject: [PATCH 1/3] Add option to show all packages --- .../CHANGELOG.md | 3 + .../src/components/VulnerabilityChecker.vue | 206 +++++++----------- .../components/vulnerability/PackageCard.vue | 64 ++++++ .../vulnerability/PackageHeader.vue | 11 +- .../components/vulnerability/PackageList.vue | 21 ++ .../src/components/vulnerability/StatCard.vue | 49 +++++ .../components/vulnerability/StatsPanel.vue | 67 +++--- .../vulnerability/VulnerabilityCard.vue | 58 ----- .../vulnerability/VulnerabilityList.vue | 32 --- .../src/stores/vulns.ts | 77 +++++++ .../src/types/index.ts | 8 + 11 files changed, 337 insertions(+), 259 deletions(-) create mode 100644 extensions/package-vulnerability-scanner/src/components/vulnerability/PackageCard.vue create mode 100644 extensions/package-vulnerability-scanner/src/components/vulnerability/PackageList.vue create mode 100644 extensions/package-vulnerability-scanner/src/components/vulnerability/StatCard.vue delete mode 100644 extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityCard.vue delete mode 100644 extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityList.vue create mode 100644 extensions/package-vulnerability-scanner/src/types/index.ts diff --git a/extensions/package-vulnerability-scanner/CHANGELOG.md b/extensions/package-vulnerability-scanner/CHANGELOG.md index cc0a4b86..ef6e62ce 100644 --- a/extensions/package-vulnerability-scanner/CHANGELOG.md +++ b/extensions/package-vulnerability-scanner/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Render markdown in vulnerability details using markdown-it (#204) - Display language versions (Python, R, Quarto) for content items (#206) +- The counts of packages are now clickable giving the option to show all + packages, only Python packages, only R packages, or only vulnerable packages. + The list defaults to showing vulnerable packages. (#207) ### Changed diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue index 57bd61cb..d52e0ac0 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue @@ -2,38 +2,41 @@ import { useVulnsStore } from "../stores/vulns"; import { usePackagesStore } from "../stores/packages"; import { useContentStore } from "../stores/content"; -import type { Vulnerability, VulnerabilityRange } from "../stores/vulns"; -import type { Package } from "../stores/packages"; -import { computed } from "vue"; +import { computed, ref } from "vue"; // Import UI components import LoadingSpinner from "./ui/LoadingSpinner.vue"; import StatusMessage from "./ui/StatusMessage.vue"; // Import vulnerability components -import StatsPanel from "./vulnerability/StatsPanel.vue"; +import StatsPanel, { type FilterType } from "./vulnerability/StatsPanel.vue"; import EmptyState from "./vulnerability/EmptyState.vue"; -import VulnerabilityList from "./vulnerability/VulnerabilityList.vue"; +import PackageList from "./vulnerability/PackageList.vue"; import ArrowTopRight from "./icons/ArrowTopRight.vue"; - -interface VulnerablePackageItem { - packageInfo: Package; - vulnerabilities: Vulnerability[]; - repo: "pypi" | "cran"; - latestFixedVersion: string | null; -} +import type { PackageWithVulnsAndFix } from "../types"; const vulnStore = useVulnsStore(); const packagesStore = usePackagesStore(); const contentStore = useContentStore(); -const packages = computed(() => { +const packages = computed((): PackageWithVulnsAndFix[] => { // Get the current content's packages const currentId = contentStore.currentContentId; if (!currentId) return []; const contentItem = packagesStore.contentItems[currentId]; - return contentItem ? contentItem.packages : []; + const result = contentItem ? contentItem.packages : []; + + return result.map((pkg): PackageWithVulnsAndFix => { + return { + package: pkg, + ...vulnStore.getDetailsForPackageVersion( + pkg.name, + pkg.version, + pkg.language.toLowerCase() === "python" ? "pypi" : "cran", + ), + }; + }); }); // Track loading states @@ -62,29 +65,6 @@ const hasPackages = computed(() => { return packages.value.length > 0; }); -// Extract the fixed version from the vulnerability ranges data -function getFixedVersion(vuln: Vulnerability): string | null { - if (!vuln.ranges || !Array.isArray(vuln.ranges) || vuln.ranges.length === 0) { - return null; - } - - let result: string | null = null; - - const getFixedEventValue = (range: VulnerabilityRange): string | null => { - return range.events.find((e) => Boolean(e.fixed))?.fixed || null; - }; - - for (const range of vuln.ranges) { - if (range.type === "ECOSYSTEM" && range.events) { - return getFixedEventValue(range); - } else { - result = getFixedEventValue(range); - } - } - - return result; -} - // Go back to content list function goBack() { contentStore.currentContentId = undefined; @@ -92,82 +72,9 @@ function goBack() { } // Find vulnerable packages by comparing package data with vulnerability data -const vulnerablePackages = computed(() => { - if (isLoading.value || !packages.value.length || !vulnStore.isFetched) - return []; - - // Use a Map to group vulnerabilities by package - const packageMap = new Map< - string, - { - packageInfo: Package; - vulnerabilities: Vulnerability[]; - repo: "pypi" | "cran"; - fixedVersions: string[]; - } - >(); - - // Process each installed package - for (const pkg of packages.value) { - const packageId = `${pkg.name}@${pkg.version}`; - const repo = pkg.language.toLowerCase() === "python" ? "pypi" : "cran"; - const vulnerabilityMap = repo === "pypi" ? vulnStore.pypi : vulnStore.cran; - const packageName = pkg.name.toLowerCase(); - - // If this package has known vulnerabilities - if (vulnerabilityMap[packageName]) { - // For each vulnerability associated with this package - for (const vuln of vulnerabilityMap[packageName]) { - // Check if the current package version is in the vulnerable versions - if (vuln.versions && vuln.versions[pkg.version]) { - const fixedVersion = getFixedVersion(vuln); - - if (!packageMap.has(packageId)) { - packageMap.set(packageId, { - packageInfo: pkg, - vulnerabilities: [], - repo, - fixedVersions: [], - }); - } - - const packageData = packageMap.get(packageId)!; - packageData.vulnerabilities.push(vuln); - - if (fixedVersion) { - packageData.fixedVersions.push(fixedVersion); - } - } - } - } - } - - // Convert the Map to an array and determine the latest fixed version - return Array.from(packageMap.values()).map((item) => { - // Sort fixed versions semantically (assuming they are valid semver) - // This simple comparison works for most simple version formats - const sortedFixedVersions = [...item.fixedVersions].sort((a, b) => { - const aParts = a.split("."); - const bParts = b.split("."); - - for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { - const aNum = parseInt(aParts[i] || "0", 10); - const bNum = parseInt(bParts[i] || "0", 10); - if (aNum !== bNum) { - return bNum - aNum; // Descending order (latest first) - } - } - - return 0; - }); - - return { - packageInfo: item.packageInfo, - vulnerabilities: item.vulnerabilities, - repo: item.repo, - latestFixedVersion: - sortedFixedVersions.length > 0 ? sortedFixedVersions[0] : null, - }; +const vulnerablePackages = computed((): PackageWithVulnsAndFix[] => { + return packages.value.filter((pkg) => { + return pkg.vulnerabilities && pkg.vulnerabilities.length > 0; }); }); @@ -190,22 +97,57 @@ const contentTitle = computed( ); const dashboardUrl = computed(() => contentInfo.value?.dashboard_url || null); -// Stats -const totalPackages = computed(() => packages.value.length); -const pythonPackages = computed( - () => - packages.value.filter((p) => p.language.toLowerCase() === "python").length, -); -const rPackages = computed( - () => packages.value.filter((p) => p.language.toLowerCase() === "r").length, -); +const pythonPackages = computed((): PackageWithVulnsAndFix[] => { + if (isLoading.value || !packages.value.length) return []; + return packages.value.filter( + (p) => p.package.language.toLowerCase() === "python", + ); +}); + +const rPackages = computed((): PackageWithVulnsAndFix[] => { + if (isLoading.value || !packages.value.length) return []; + return packages.value.filter((p) => p.package.language.toLowerCase() === "r"); +}); // Total number of vulnerabilities (CVEs) across all packages const totalVulnerabilities = computed(() => { return vulnerablePackages.value.reduce((total, pkg) => { - return total + pkg.vulnerabilities.length; + return total + (pkg.vulnerabilities ? pkg.vulnerabilities.length : 0); }, 0); }); + +const activeFilter = ref("vulnerable"); + +const filterTitle = computed(() => { + switch (activeFilter.value) { + case "all": + return "All Packages"; + case "python": + return "Python Packages"; + case "r": + return "R Packages"; + case "vulnerable": + return "Vulnerable Packages"; + default: + return "Packages"; + } +}); + +// Use the filtered arrays for the displayed packages +const filteredPackages = computed((): PackageWithVulnsAndFix[] => { + if (isLoading.value || !packages.value.length) return []; + + switch (activeFilter.value) { + case "python": + return pythonPackages.value; + case "r": + return rPackages.value; + case "vulnerable": + return vulnerablePackages.value; + default: + return packages.value; + } +}); diff --git a/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageCard.vue b/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageCard.vue new file mode 100644 index 00000000..a94daef9 --- /dev/null +++ b/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageCard.vue @@ -0,0 +1,64 @@ + + + diff --git a/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageHeader.vue b/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageHeader.vue index 9d73e56e..af9ed331 100644 --- a/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageHeader.vue +++ b/extensions/package-vulnerability-scanner/src/components/vulnerability/PackageHeader.vue @@ -1,18 +1,19 @@