Skip to content

Commit d9bcbe5

Browse files
committed
⚡ perf(ui): memoize safe URL computation for vulnerability list
Compute toSafeExternalUrl once per vulnerability in a computed property instead of calling it per v-if/v-bind in the template loop.
1 parent 66873a6 commit d9bcbe5

2 files changed

Lines changed: 64 additions & 5 deletions

File tree

ui/src/views/SecurityView.vue

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ const selectedImageVulns = computed(() => {
149149
return vulnerabilitiesByImage.value[selectedImage.value.image] || [];
150150
});
151151
152+
const selectedImageVulnsWithSafeUrl = computed(() =>
153+
selectedImageVulns.value.map((vuln) => ({
154+
...vuln,
155+
safePrimaryUrl: toSafeExternalUrl(vuln.primaryUrl),
156+
})),
157+
);
158+
152159
function openDetail(summary: ImageSummary) {
153160
const vulnerabilities = vulnerabilitiesByImage.value[summary.image] || [];
154161
openSbomDetail({
@@ -627,7 +634,7 @@ onUnmounted(() => {
627634

628635
<!-- Vulnerability list -->
629636
<div class="divide-y" :style="{ borderColor: 'var(--dd-border)' }">
630-
<div v-for="vuln in selectedImageVulns" :key="vuln.id + vuln.package"
637+
<div v-for="vuln in selectedImageVulnsWithSafeUrl" :key="vuln.id + vuln.package"
631638
class="px-4 py-3 hover:dd-bg-hover transition-colors">
632639
<div class="flex items-center gap-2 mb-1.5">
633640
<AppIcon :name="severityIcon(vuln.severity)" :size="12"
@@ -647,7 +654,7 @@ onUnmounted(() => {
647654
<span v-else class="ml-auto text-2xs dd-text-muted">No fix</span>
648655
</div>
649656
<div
650-
v-if="vuln.title || vuln.target || toSafeExternalUrl(vuln.primaryUrl)"
657+
v-if="vuln.title || vuln.target || vuln.safePrimaryUrl"
651658
class="ml-5 mt-1.5 space-y-1"
652659
>
653660
<div v-if="vuln.title" class="text-2xs dd-text">
@@ -658,8 +665,8 @@ onUnmounted(() => {
658665
<span class="font-mono dd-text">{{ vuln.target }}</span>
659666
</div>
660667
<a
661-
v-if="toSafeExternalUrl(vuln.primaryUrl)"
662-
:href="toSafeExternalUrl(vuln.primaryUrl) || undefined"
668+
v-if="vuln.safePrimaryUrl"
669+
:href="vuln.safePrimaryUrl"
663670
target="_blank"
664671
rel="noopener noreferrer"
665672
class="inline-flex text-2xs underline hover:no-underline break-all"

ui/tests/views/SecurityView.spec.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ const mockGetContainerSbom = vi.fn();
77
const mockGetSecurityRuntime = vi.fn();
88
const mockIsMobile = { value: false };
99
const mockWindowNarrow = { value: false };
10-
const { mockComputeSecurityDelta } = vi.hoisted(() => ({
10+
const { mockComputeSecurityDelta, mockToSafeExternalUrl } = vi.hoisted(() => ({
1111
mockComputeSecurityDelta: vi.fn(),
12+
mockToSafeExternalUrl: vi.fn(),
1213
}));
1314

1415
vi.mock('@/services/container', () => ({
@@ -37,6 +38,19 @@ vi.mock('@/utils/container-mapper', async () => {
3738
};
3839
});
3940

41+
vi.mock('@/views/security/securityViewUtils', async () => {
42+
const actual = await vi.importActual<typeof import('@/views/security/securityViewUtils')>(
43+
'@/views/security/securityViewUtils',
44+
);
45+
return {
46+
...actual,
47+
toSafeExternalUrl: (...args: Parameters<typeof actual.toSafeExternalUrl>) => {
48+
mockToSafeExternalUrl(...args);
49+
return actual.toSafeExternalUrl(...args);
50+
},
51+
};
52+
});
53+
4054
import { mount } from '@vue/test-utils';
4155
import { clearIconCache, updateSettings } from '@/services/settings';
4256
import SecurityView from '@/views/SecurityView.vue';
@@ -465,6 +479,44 @@ describe('SecurityView', () => {
465479
expect(w.find('a[href="https://avd.aquasec.com/nvd/cve-2026-9999"]').exists()).toBe(true);
466480
});
467481

482+
it('computes safe vulnerability URLs once per vulnerability instead of per binding', async () => {
483+
mockContainers([
484+
makeContainer({
485+
id: 'container-1',
486+
displayName: 'nginx',
487+
security: {
488+
scan: {
489+
vulnerabilities: [
490+
{
491+
id: 'CVE-2026-9999',
492+
severity: 'CRITICAL',
493+
packageName: 'openssl',
494+
installedVersion: '3.0.0',
495+
fixedVersion: '3.0.10',
496+
title: 'OpenSSL buffer overflow',
497+
target: 'usr/lib/libcrypto.so',
498+
primaryUrl: 'https://avd.aquasec.com/nvd/cve-2026-9999',
499+
},
500+
],
501+
},
502+
},
503+
}),
504+
]);
505+
506+
const w = factory();
507+
await vi.waitFor(() => expect(mockGetSecurityVulnerabilityOverview).toHaveBeenCalledOnce());
508+
await flushPromises();
509+
510+
const vm = w.vm as any;
511+
vm.openDetail(vm.filteredSummaries[0]);
512+
await flushPromises();
513+
514+
expect(mockToSafeExternalUrl).toHaveBeenCalledTimes(1);
515+
expect(mockToSafeExternalUrl).toHaveBeenCalledWith(
516+
'https://avd.aquasec.com/nvd/cve-2026-9999',
517+
);
518+
});
519+
468520
it('does not render vulnerability links for disallowed URL protocols', async () => {
469521
mockContainers([
470522
makeContainer({

0 commit comments

Comments
 (0)