From 2436c01475a724d77be508a0237a0eeced36c9da Mon Sep 17 00:00:00 2001 From: Amy Lin Date: Wed, 29 Jul 2026 11:43:13 -0500 Subject: [PATCH 1/4] feat(package-vulnerability-scanner): show the specific reason a scan or read failed VulnerabilityChecker.vue and ContentCard.vue now show the actual detail (content.packageFetchError.message, scannerStore.scanError) instead of a one-size-fits-all message, and a "Not scanned" badge distinguishes a failed scan from "no vulnerabilities found". Adds a loading state for the vulnerability lookup itself, so a card with loaded packages doesn't briefly report "no vulnerabilities" before the scan actually finishes. --- .../manifest.json | 6 +- .../src/components/ContentCard.vue | 6 +- .../components/VulnerabilityChecker.test.ts | 59 +++++++++++++++++++ .../src/components/VulnerabilityChecker.vue | 22 ++++++- .../vulnerability/VulnerabilityDetails.vue | 2 +- 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts diff --git a/extensions/package-vulnerability-scanner/manifest.json b/extensions/package-vulnerability-scanner/manifest.json index 0776c48f..f6fe8014 100644 --- a/extensions/package-vulnerability-scanner/manifest.json +++ b/extensions/package-vulnerability-scanner/manifest.json @@ -23,11 +23,11 @@ "dist/assets/index-CKUnK_gI.css": { "checksum": "910900dfbf81f7ac4caf125b844a495c" }, - "dist/assets/index-BR_SAMDl.js": { - "checksum": "1641c59b4d18d7029985a3b98890a397" + "dist/assets/index-PKuzyQYK.js": { + "checksum": "bd612838b6ccf0c3a03241de2048eec3" }, "dist/index.html": { - "checksum": "5bb81dbee09e839af32e17caedc8a436" + "checksum": "bdc6e932f6c401d4db24a1157178352a" }, "main.py": { "checksum": "ea913b00aaac84531ee46021969f4eaa" diff --git a/extensions/package-vulnerability-scanner/src/components/ContentCard.vue b/extensions/package-vulnerability-scanner/src/components/ContentCard.vue index 8877d78b..58910ba1 100644 --- a/extensions/package-vulnerability-scanner/src/components/ContentCard.vue +++ b/extensions/package-vulnerability-scanner/src/components/ContentCard.vue @@ -54,6 +54,10 @@ function handleClick() { Loading... + + Not scanned + + {{ vulnerabilityText }} @@ -88,7 +92,7 @@ function handleClick() { Incomplete content - Error loading packages + Couldn't load packages Loading packages... diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts new file mode 100644 index 00000000..9ee33a8e --- /dev/null +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts @@ -0,0 +1,59 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { mount } from "@vue/test-utils"; + +import VulnerabilityChecker from "./VulnerabilityChecker.vue"; +import type { Content } from "../stores/scanner"; +import { useVulnsStore } from "../stores/vulns"; + +function makeContent(overrides: Partial = {}): Content { + return { + guid: "g1", + title: "My Report", + bundle_id: "b1", + packages: [], + vulnerabilityCount: 0, + isLoadingPackages: false, + ...overrides, + } as Content; +} + +describe("VulnerabilityChecker", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("shows the specific reason from Connect when packages can't be read", () => { + const content = makeContent({ + packageFetchError: new Error("Connect API error: Content not found"), + }); + + const wrapper = mount(VulnerabilityChecker, { props: { content } }); + + expect(wrapper.text()).toContain("Couldn't load this content's packages"); + expect(wrapper.text()).toContain("Connect API error: Content not found"); + }); + + it("shows a loading state, not a verdict, while the vuln lookup is pending", () => { + // A fresh vulns store has not fetched yet, so the scan is still in progress + // even though this item's packages have loaded. + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Checking for vulnerabilities"); + expect(wrapper.text()).not.toContain("No vulnerabilities found"); + }); + + it("shows the result once the scan has completed", () => { + useVulnsStore().isFetched = true; // scan no longer in progress + + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).not.toContain("Checking for vulnerabilities"); + expect(wrapper.text()).toContain("No vulnerabilities found"); + }); +}); diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue index d952eab4..e0267682 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue @@ -126,7 +126,7 @@ const hasLanguageVersion = computed(() => { v-if="content.dashboard_url" :href="content.dashboard_url" target="_blank" - rel="noopener noreferrer" + rel="noopener" class="text-sm text-blue-600 hover:text-blue-800 flex items-center" > View on Connect @@ -162,7 +162,7 @@ const hasLanguageVersion = computed(() => { /> @@ -170,7 +170,23 @@ const hasLanguageVersion = computed(() => { + + + + + diff --git a/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue b/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue index 5101be16..a46d32ed 100644 --- a/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue +++ b/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue @@ -32,7 +32,7 @@ function getOsvUrl(vulnId: string): string { {{ id }} From 076148bbde869c4c0ce3d1e957d387be84801fc2 Mon Sep 17 00:00:00 2001 From: Amy Lin Date: Fri, 31 Jul 2026 20:32:56 -0500 Subject: [PATCH 2/4] test(package-vulnerability-scanner): cover the scan-failed badge and status message --- .../src/components/ContentCard.test.ts | 71 +++++++++++++++++++ .../components/VulnerabilityChecker.test.ts | 20 ++++++ 2 files changed, 91 insertions(+) create mode 100644 extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts diff --git a/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts b/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts new file mode 100644 index 00000000..5d4f0d65 --- /dev/null +++ b/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { mount } from "@vue/test-utils"; + +import ContentCard from "./ContentCard.vue"; +import type { Content } from "../stores/scanner"; +import { useVulnsStore } from "../stores/vulns"; + +function makeContent(overrides: Partial = {}): Content { + return { + guid: "g1", + title: "My Report", + bundle_id: "b1", + packages: [], + vulnerabilityCount: 0, + isLoadingPackages: false, + ...overrides, + } as Content; +} + +describe("ContentCard", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("shows a Not scanned badge when the scan has failed", () => { + useVulnsStore().error = new Error( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + + const wrapper = mount(ContentCard, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Not scanned"); + expect(wrapper.text()).not.toContain("No vulnerabilities"); + }); + + it("shows a Loading badge while the scan is in progress", () => { + // A fresh vulns store has not fetched yet, so the scan is still in progress. + const wrapper = mount(ContentCard, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Loading..."); + }); + + it("shows the vulnerability count once the scan has completed", () => { + useVulnsStore().isFetched = true; // scan no longer in progress + + const wrapper = mount(ContentCard, { + props: { content: makeContent({ vulnerabilityCount: 2 }) }, + }); + + expect(wrapper.text()).toContain("2 vulnerabilities"); + }); + + it("hides the scan badge entirely when this item's own packages failed to load", () => { + useVulnsStore().error = new Error("vuln service down"); + + const wrapper = mount(ContentCard, { + props: { + content: makeContent({ packageFetchError: new Error("boom") }), + }, + }); + + expect(wrapper.text()).not.toContain("Not scanned"); + expect(wrapper.text()).not.toContain("Loading..."); + }); +}); diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts index 9ee33a8e..be5e3d7b 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts @@ -6,6 +6,7 @@ import { mount } from "@vue/test-utils"; import VulnerabilityChecker from "./VulnerabilityChecker.vue"; import type { Content } from "../stores/scanner"; import { useVulnsStore } from "../stores/vulns"; +import { usePackagesStore } from "../stores/packages"; function makeContent(overrides: Partial = {}): Content { return { @@ -35,6 +36,25 @@ describe("VulnerabilityChecker", () => { expect(wrapper.text()).toContain("Connect API error: Content not found"); }); + it("shows the scan failure reason when the vuln lookup itself fails", () => { + // Unlike packageFetchError (this item's own packages), scanFailed reflects a + // store-wide failure (e.g. the vulnerability service), so it applies even + // though this item's packages loaded fine. + usePackagesStore().error = null; + useVulnsStore().error = new Error( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Scan couldn't complete"); + expect(wrapper.text()).toContain( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + }); + it("shows a loading state, not a verdict, while the vuln lookup is pending", () => { // A fresh vulns store has not fetched yet, so the scan is still in progress // even though this item's packages have loaded. From bb498c3d2cc551cbccf5e501a9a4880ce18e092d Mon Sep 17 00:00:00 2001 From: Amy Lin Date: Fri, 31 Jul 2026 22:49:32 -0500 Subject: [PATCH 3/4] fix(package-vulnerability-scanner): restore rel=noreferrer on external links, dropped unintentionally --- .../src/components/VulnerabilityChecker.vue | 2 +- .../src/components/vulnerability/VulnerabilityDetails.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue index e0267682..7ac57ffc 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue @@ -126,7 +126,7 @@ const hasLanguageVersion = computed(() => { v-if="content.dashboard_url" :href="content.dashboard_url" target="_blank" - rel="noopener" + rel="noopener noreferrer" class="text-sm text-blue-600 hover:text-blue-800 flex items-center" > View on Connect diff --git a/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue b/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue index a46d32ed..5101be16 100644 --- a/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue +++ b/extensions/package-vulnerability-scanner/src/components/vulnerability/VulnerabilityDetails.vue @@ -32,7 +32,7 @@ function getOsvUrl(vulnId: string): string { {{ id }} From 78e2dcbc1058e0a0c42ddca94dc49f7ac37719d3 Mon Sep 17 00:00:00 2001 From: Amy Lin Date: Fri, 31 Jul 2026 23:46:44 -0500 Subject: [PATCH 4/4] style(package-vulnerability-scanner): drop a redundant optional chain inside its own v-else-if Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/VulnerabilityChecker.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue index 7ac57ffc..0d048ad2 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue @@ -171,7 +171,7 @@ const hasLanguageVersion = computed(() => { v-else-if="content.packageFetchError" type="error" message="Couldn't load this content's packages" - :details="content.packageFetchError?.message" + :details="content.packageFetchError.message" />