22import { useVulnsStore } from " ../stores/vulns" ;
33import { usePackagesStore } from " ../stores/packages" ;
44import { useContentStore } from " ../stores/content" ;
5- import type { Vulnerability , VulnerabilityRange } from " ../stores/vulns" ;
6- import type { Package } from " ../stores/packages" ;
7- import { computed } from " vue" ;
5+ import { computed , ref } from " vue" ;
86
97// Import UI components
108import LoadingSpinner from " ./ui/LoadingSpinner.vue" ;
119import StatusMessage from " ./ui/StatusMessage.vue" ;
1210
1311// Import vulnerability components
14- import StatsPanel from " ./vulnerability/StatsPanel.vue" ;
12+ import StatsPanel , { type FilterType } from " ./vulnerability/StatsPanel.vue" ;
1513import EmptyState from " ./vulnerability/EmptyState.vue" ;
16- import VulnerabilityList from " ./vulnerability/VulnerabilityList .vue" ;
14+ import PackageList from " ./vulnerability/PackageList .vue" ;
1715import ArrowTopRight from " ./icons/ArrowTopRight.vue" ;
18-
19- interface VulnerablePackageItem {
20- packageInfo: Package ;
21- vulnerabilities: Vulnerability [];
22- repo: " pypi" | " cran" ;
23- latestFixedVersion: string | null ;
24- }
16+ import type { PackageWithVulnsAndFix } from " ../types" ;
2517
2618const vulnStore = useVulnsStore ();
2719const packagesStore = usePackagesStore ();
2820const contentStore = useContentStore ();
2921
30- const packages = computed (() => {
22+ const packages = computed ((): PackageWithVulnsAndFix [] => {
3123 // Get the current content's packages
3224 const currentId = contentStore .currentContentId ;
3325 if (! currentId ) return [];
3426
3527 const contentItem = packagesStore .contentItems [currentId ];
36- return contentItem ? contentItem .packages : [];
28+ const result = contentItem ? contentItem .packages : [];
29+
30+ return result .map ((pkg ): PackageWithVulnsAndFix => {
31+ return {
32+ package: pkg ,
33+ ... vulnStore .getDetailsForPackageVersion (
34+ pkg .name ,
35+ pkg .version ,
36+ pkg .language .toLowerCase () === " python" ? " pypi" : " cran" ,
37+ ),
38+ };
39+ });
3740});
3841
3942// Track loading states
@@ -62,112 +65,16 @@ const hasPackages = computed(() => {
6265 return packages .value .length > 0 ;
6366});
6467
65- // Extract the fixed version from the vulnerability ranges data
66- function getFixedVersion(vuln : Vulnerability ): string | null {
67- if (! vuln .ranges || ! Array .isArray (vuln .ranges ) || vuln .ranges .length === 0 ) {
68- return null ;
69- }
70-
71- let result: string | null = null ;
72-
73- const getFixedEventValue = (range : VulnerabilityRange ): string | null => {
74- return range .events .find ((e ) => Boolean (e .fixed ))?.fixed || null ;
75- };
76-
77- for (const range of vuln .ranges ) {
78- if (range .type === " ECOSYSTEM" && range .events ) {
79- return getFixedEventValue (range );
80- } else {
81- result = getFixedEventValue (range );
82- }
83- }
84-
85- return result ;
86- }
87-
8868// Go back to content list
8969function goBack() {
9070 contentStore .currentContentId = undefined ;
9171 scrollTo ({ top: 0 , left: 0 , behavior: " instant" });
9272}
9373
9474// Find vulnerable packages by comparing package data with vulnerability data
95- const vulnerablePackages = computed <VulnerablePackageItem []>(() => {
96- if (isLoading .value || ! packages .value .length || ! vulnStore .isFetched )
97- return [];
98-
99- // Use a Map to group vulnerabilities by package
100- const packageMap = new Map <
101- string ,
102- {
103- packageInfo : Package ;
104- vulnerabilities : Vulnerability [];
105- repo : " pypi" | " cran" ;
106- fixedVersions : string [];
107- }
108- > ();
109-
110- // Process each installed package
111- for (const pkg of packages .value ) {
112- const packageId = ` ${pkg .name }@${pkg .version } ` ;
113- const repo = pkg .language .toLowerCase () === " python" ? " pypi" : " cran" ;
114- const vulnerabilityMap = repo === " pypi" ? vulnStore .pypi : vulnStore .cran ;
115- const packageName = pkg .name .toLowerCase ();
116-
117- // If this package has known vulnerabilities
118- if (vulnerabilityMap [packageName ]) {
119- // For each vulnerability associated with this package
120- for (const vuln of vulnerabilityMap [packageName ]) {
121- // Check if the current package version is in the vulnerable versions
122- if (vuln .versions && vuln .versions [pkg .version ]) {
123- const fixedVersion = getFixedVersion (vuln );
124-
125- if (! packageMap .has (packageId )) {
126- packageMap .set (packageId , {
127- packageInfo: pkg ,
128- vulnerabilities: [],
129- repo ,
130- fixedVersions: [],
131- });
132- }
133-
134- const packageData = packageMap .get (packageId )! ;
135- packageData .vulnerabilities .push (vuln );
136-
137- if (fixedVersion ) {
138- packageData .fixedVersions .push (fixedVersion );
139- }
140- }
141- }
142- }
143- }
144-
145- // Convert the Map to an array and determine the latest fixed version
146- return Array .from (packageMap .values ()).map ((item ) => {
147- // Sort fixed versions semantically (assuming they are valid semver)
148- // This simple comparison works for most simple version formats
149- const sortedFixedVersions = [... item .fixedVersions ].sort ((a , b ) => {
150- const aParts = a .split (" ." );
151- const bParts = b .split (" ." );
152-
153- for (let i = 0 ; i < Math .max (aParts .length , bParts .length ); i ++ ) {
154- const aNum = parseInt (aParts [i ] || " 0" , 10 );
155- const bNum = parseInt (bParts [i ] || " 0" , 10 );
156- if (aNum !== bNum ) {
157- return bNum - aNum ; // Descending order (latest first)
158- }
159- }
160-
161- return 0 ;
162- });
163-
164- return {
165- packageInfo: item .packageInfo ,
166- vulnerabilities: item .vulnerabilities ,
167- repo: item .repo ,
168- latestFixedVersion:
169- sortedFixedVersions .length > 0 ? sortedFixedVersions [0 ] : null ,
170- };
75+ const vulnerablePackages = computed ((): PackageWithVulnsAndFix [] => {
76+ return packages .value .filter ((pkg ) => {
77+ return pkg .vulnerabilities && pkg .vulnerabilities .length > 0 ;
17178 });
17279});
17380
@@ -190,22 +97,57 @@ const contentTitle = computed(
19097);
19198const dashboardUrl = computed (() => contentInfo .value ?.dashboard_url || null );
19299
193- // Stats
194- const totalPackages = computed (() => packages .value .length );
195- const pythonPackages = computed (
196- () =>
197- packages .value .filter ((p ) => p .language .toLowerCase () === " python" ).length ,
198- );
199- const rPackages = computed (
200- () => packages .value .filter ((p ) => p .language .toLowerCase () === " r" ).length ,
201- );
100+ const pythonPackages = computed ((): PackageWithVulnsAndFix [] => {
101+ if (isLoading .value || ! packages .value .length ) return [];
102+ return packages .value .filter (
103+ (p ) => p .package .language .toLowerCase () === " python" ,
104+ );
105+ });
106+
107+ const rPackages = computed ((): PackageWithVulnsAndFix [] => {
108+ if (isLoading .value || ! packages .value .length ) return [];
109+ return packages .value .filter ((p ) => p .package .language .toLowerCase () === " r" );
110+ });
202111
203112// Total number of vulnerabilities (CVEs) across all packages
204113const totalVulnerabilities = computed (() => {
205114 return vulnerablePackages .value .reduce ((total , pkg ) => {
206- return total + pkg .vulnerabilities . length ;
115+ return total + ( pkg .vulnerabilities ? pkg . vulnerabilities . length : 0 ) ;
207116 }, 0 );
208117});
118+
119+ const activeFilter = ref <FilterType >(" vulnerable" );
120+
121+ const filterTitle = computed (() => {
122+ switch (activeFilter .value ) {
123+ case " all" :
124+ return " All Packages" ;
125+ case " python" :
126+ return " Python Packages" ;
127+ case " r" :
128+ return " R Packages" ;
129+ case " vulnerable" :
130+ return " Vulnerable Packages" ;
131+ default :
132+ return " Packages" ;
133+ }
134+ });
135+
136+ // Use the filtered arrays for the displayed packages
137+ const filteredPackages = computed ((): PackageWithVulnsAndFix [] => {
138+ if (isLoading .value || ! packages .value .length ) return [];
139+
140+ switch (activeFilter .value ) {
141+ case " python" :
142+ return pythonPackages .value ;
143+ case " r" :
144+ return rPackages .value ;
145+ case " vulnerable" :
146+ return vulnerablePackages .value ;
147+ default :
148+ return packages .value ;
149+ }
150+ });
209151 </script >
210152
211153<template >
@@ -266,21 +208,27 @@ const totalVulnerabilities = computed(() => {
266208 <!-- Content loaded successfully -->
267209 <template v-else >
268210 <StatsPanel
269- :totalPackages =" totalPackages"
270- :pythonPackages =" pythonPackages"
271- :rPackages =" rPackages"
211+ v-model =" activeFilter"
212+ :totalPackages =" packages.length"
213+ :pythonPackages =" pythonPackages.length"
214+ :rPackages =" rPackages.length"
272215 :vulnerabilities =" totalVulnerabilities"
273216 />
274217
218+ <h3 class =" text-lg text-gray-700 mb-4" >{{ filterTitle }}</h3 >
219+
275220 <EmptyState v-if =" !hasPackages" >
276221 <p >This content has no packages. No vulnerabilities found.</p >
277222 </EmptyState >
278223
279- <EmptyState v-else-if =" vulnerablePackages.length === 0" >
280- <p >No vulnerabilities found.</p >
224+ <EmptyState v-else-if =" filteredPackages.length === 0" >
225+ <p v-if =" activeFilter === 'vulnerable'" >No vulnerabilities found.</p >
226+ <p v-else-if =" activeFilter === 'python'" >No Python packages found.</p >
227+ <p v-else-if =" activeFilter === 'r'" >No R packages found.</p >
228+ <p v-else >No packages found.</p >
281229 </EmptyState >
282230
283- <VulnerabilityList v-else :vulnerablePackages = " vulnerablePackages " />
231+ <PackageList v-else :packages = " filteredPackages " />
284232 </template >
285233 </div >
286234</template >
0 commit comments