Skip to content

Commit 7b83f24

Browse files
authored
Add ability to show all packages or just language specific packages (#222)
* Add option to show all packages * Update manifest checksums * Make the StatCard a button
1 parent bd07e17 commit 7b83f24

File tree

12 files changed

+343
-264
lines changed

12 files changed

+343
-264
lines changed

extensions/package-vulnerability-scanner/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Render markdown in vulnerability details using markdown-it (#204)
1313
- Display language versions (Python, R, Quarto) for content items (#206)
14+
- The counts of packages are now clickable giving the option to show all
15+
packages, only Python packages, only R packages, or only vulnerable packages.
16+
The list defaults to showing vulnerable packages. (#207)
1417

1518
### Changed
1619

extensions/package-vulnerability-scanner/manifest.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
},
2121
"packages": {},
2222
"files": {
23-
"dist/assets/index-D8PwKrP_.css": {
24-
"checksum": "044d9467fefba864d607aec25d40b752"
23+
"dist/assets/index-CmdH3BdM.css": {
24+
"checksum": "133200803297edaaba634130f05ce6ac"
2525
},
26-
"dist/assets/index-DZg-mVL2.js": {
27-
"checksum": "f2669c5c9adb7f51e92ca4703695fecb"
26+
"dist/assets/index-Mo0hYq-0.js": {
27+
"checksum": "a8cd717c6740df720243f5c5df7440a3"
2828
},
2929
"dist/index.html": {
30-
"checksum": "2dcc09528bdcb3616a22cf8940867014"
30+
"checksum": "4a7eba688b0cb49d2d5cc87c123fbcfd"
3131
},
3232
"main.py": {
3333
"checksum": "f8385dbd8a8cd24204f1eb6209f8bb30"

extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue

Lines changed: 77 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,41 @@
22
import { useVulnsStore } from "../stores/vulns";
33
import { usePackagesStore } from "../stores/packages";
44
import { 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
108
import LoadingSpinner from "./ui/LoadingSpinner.vue";
119
import 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";
1513
import EmptyState from "./vulnerability/EmptyState.vue";
16-
import VulnerabilityList from "./vulnerability/VulnerabilityList.vue";
14+
import PackageList from "./vulnerability/PackageList.vue";
1715
import 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
2618
const vulnStore = useVulnsStore();
2719
const packagesStore = usePackagesStore();
2820
const 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
8969
function 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
);
19198
const 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
204113
const 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>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import { computed } from "vue";
3+
4+
import PackageHeader from "./PackageHeader.vue";
5+
import VulnerabilityDetails from "./VulnerabilityDetails.vue";
6+
import type { Package } from "../../stores/packages";
7+
import { type Vulnerability } from "../../stores/vulns";
8+
9+
const props = defineProps<{
10+
package: Package;
11+
vulnerabilities?: Vulnerability[];
12+
latestFixedVersion?: string | null;
13+
}>();
14+
15+
const repo = computed(() =>
16+
props.package.language.toLowerCase() === "python" ? "pypi" : "cran",
17+
);
18+
</script>
19+
20+
<template>
21+
<div>
22+
<PackageHeader :package="package" :repo="repo" />
23+
24+
<template v-if="vulnerabilities && vulnerabilities.length > 0">
25+
<p
26+
v-if="vulnerabilities.length > 1"
27+
class="text-sm font-medium text-gray-700 mb-3"
28+
>
29+
{{ vulnerabilities.length }} vulnerabilities found for this package
30+
</p>
31+
32+
<div
33+
v-if="latestFixedVersion"
34+
class="bg-green-50 border-l-3 border-green-500 py-2 px-3 my-3 rounded-r-md"
35+
>
36+
<p class="m-0 text-[15px] text-green-700 flex items-center">
37+
<span class="mr-2">🛠️</span>
38+
<span
39+
>To fix
40+
{{
41+
vulnerabilities.length > 1
42+
? "these vulnerabilities"
43+
: "this vulnerability"
44+
}}, upgrade to version {{ latestFixedVersion }} or later.</span
45+
>
46+
</p>
47+
</div>
48+
49+
<template
50+
v-for="vulnerability in vulnerabilities"
51+
:key="vulnerability.id"
52+
>
53+
<VulnerabilityDetails
54+
:id="vulnerability.id"
55+
class="not-last:mb-6"
56+
:summary="vulnerability.summary"
57+
:details="vulnerability.details"
58+
:publishDate="vulnerability.published"
59+
:modifiedDate="vulnerability.modified"
60+
/>
61+
</template>
62+
</template>
63+
</div>
64+
</template>

extensions/package-vulnerability-scanner/src/components/vulnerability/PackageHeader.vue

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
<script setup lang="ts">
2+
import type { Package } from "../../stores/packages";
3+
24
defineProps<{
3-
packageName: string;
4-
packageVersion: string;
5+
package: Package;
56
repo: "pypi" | "cran";
67
}>();
78
</script>
89

910
<template>
10-
<div class="flex items-center mb-3 gap-2 font-mono">
11+
<div class="flex items-center not-last:mb-3 gap-2 font-mono">
1112
<h4 class="text-gray-800 text-lg m-0 font-semibold">
12-
{{ packageName }}
13+
{{ package.name }}
1314
</h4>
1415

15-
<span class="text-gray-500">v{{ packageVersion }}</span>
16+
<span class="text-gray-500">v{{ package.version }}</span>
1617

1718
<span
1819
class="text-xs font-bold text-white py-1 px-2 rounded self-end ml-auto"

0 commit comments

Comments
 (0)