Skip to content

Commit f7b9d05

Browse files
authored
Added vulnerability details in SBOM page (#102)
* Added sbom page * Added default comment * Added advisories link * Added default status message * Updated wording
1 parent e1c8261 commit f7b9d05

File tree

1 file changed

+267
-18
lines changed

1 file changed

+267
-18
lines changed

src/pages/sbom.astro

Lines changed: 267 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ import FlexibleSection from "../components/FlexibleSection.astro";
1212
// Per-component metadata (id, name, version, repo) to build exact URLs
1313
interface ComponentDef { id: 'defguard' | 'defguard-client' | 'defguard-mobile' | 'defguard-proxy' | 'defguard-gateway'; name: string; version: string; repo: string; status: string }
1414
const COMPONENTS: ComponentDef[] = [
15-
{ id: 'defguard', name: 'Core', version: '1.5.1', repo: 'defguard', status: 'Fix in testing' },
15+
{ id: 'defguard', name: 'Core', version: '1.5.1', repo: 'defguard', status: '' },
1616
{ id: 'defguard-proxy', name: 'Proxy', version: '1.5.1', repo: 'proxy', status: '' },
1717
{ id: 'defguard-gateway', name: 'Gateway', version: '1.5.1', repo: 'gateway', status: '' },
1818
{ id: 'defguard-client', name: 'Desktop App', version: '1.5.1', repo: 'client', status: '' },
19-
{ id: 'defguard-mobile', name: 'Mobile App', version: '1.5.1', repo: 'mobile-client', status: '' },
19+
{ id: 'defguard-mobile', name: 'Mobile App', version: '1.5.1', repo: 'mobile-client', status: '' },
2020
];
2121
2222
const buildReleaseTag = (version: string): string => `v${version}`;
23-
// const basePath = 'sboms';
2423
const basePath = 'https://github.com/DefGuard';
2524
const buildBaseUrl = (repo: string, version: string): string => `${basePath}/${repo}/releases/download/${buildReleaseTag(version)}/`;
2625
const buildSbomUrl = (c: ComponentDef): string => `${buildBaseUrl(c.repo, c.version)}${c.id}-${c.version}.sbom.json`;
@@ -40,7 +39,13 @@ const pickHighestSeverity = (items: string[]): string => {
4039
4140
// Group by component/version
4241
interface SbomEntry { name: string; version: string; url: string; status: string }
43-
interface Vulnerability { Severity?: string; VulnerabilityID?: string }
42+
interface Vulnerability {
43+
Severity?: string;
44+
VulnerabilityID?: string;
45+
PkgID?: string;
46+
PrimaryURL?: string;
47+
Title?: string;
48+
}
4449
interface AdvisoryResult { Vulnerabilities?: Vulnerability[] }
4550
interface AdvisoryFile { CreatedAt?: string; Metadata?: { CreatedAt?: string }; Results?: AdvisoryResult[] }
4651
@@ -65,12 +70,16 @@ await Promise.all(COMPONENTS.map(async (c) => {
6570
// Build components array for table
6671
const components = Array.from(sboms.values()).map((c: SbomEntry) => {
6772
const adv = advisories.get(`${c.name}@${c.version}`);
73+
// Find the original component to get advisories URL
74+
const comp = COMPONENTS.find(comp => comp.name === c.name);
75+
const advisoriesUrl = comp ? buildAdvisoriesUrl(comp) : '';
6876
return {
6977
name: c.name,
7078
format: 'JSON',
7179
version: c.version,
7280
date: adv?.createdAt ? String(adv.createdAt).slice(0, 10) : '',
7381
url: c.url,
82+
advisoriesUrl,
7483
status: c.status,
7584
};
7685
});
@@ -145,9 +154,12 @@ const tags = [
145154
<FlexibleSection leftRatio={1} title="SBOM file list with vulnerability status" theme="light">
146155
<div slot="left" class="sbom-filelist">
147156
<p>
148-
We publish separate SBOMs for <strong>mobile apps</strong> (Android, iOS), the <strong>desktop app</strong>
149-
(Windows, macOS, Linux), and <strong>server components</strong> (Core, Proxy, Gateway). We provide them in the standard format
150-
(SPDX), enabling integration with tools like Trivy, Syft, and Dependency‑Track.
157+
Separate SBOMs are available for <strong>mobile apps</strong> (Android, iOS), the <strong>desktop app</strong>
158+
(Windows, macOS, Linux), and <strong>server components</strong> (Core, Proxy, Gateway). Each SBOM is updated <strong>every day</strong> and provided in the standard
159+
<strong>SPDX</strong> format, enabling integration with tools like Trivy or Syft.
160+
</p>
161+
<p>
162+
Alongside each SBOM, <strong>advisories files</strong> are also published to summarize known vulnerabilities in detail.
151163
</p>
152164
<div class="content-measure">
153165
<table class="sbom-table" role="table" aria-label="SBOM list with vulnerability status">
@@ -156,13 +168,13 @@ const tags = [
156168
<th>Component</th>
157169
<th>Version</th>
158170
<th>Date checked</th>
159-
<th>SBOM link</th>
171+
<th>Links</th>
160172
<th>Vulnerability status</th>
161173
<th>Status</th>
162174
</tr>
163175
</thead>
164176
<tbody>
165-
{components.map((c) => {
177+
{components.map((c, index) => {
166178
const report = vulnReports.find((r) => r.component === c.name && r.version === c.version);
167179
const statusLabel = report
168180
? (report.status === 'ok' ? 'No vulnerabilities' : `${report.severity} vulnerabilities`)
@@ -171,15 +183,100 @@ const tags = [
171183
const badgeClass = report
172184
? (report.status === 'ok' ? 'ok' : (severity || 'issues'))
173185
: 'na';
186+
const hasVulns = report && report.status !== 'ok';
187+
const rowId = `component-${index}`;
188+
const detailsId = `details-${index}`;
189+
174190
return (
175-
<tr>
176-
<td>{c.name}</td>
177-
<td class="nowrap">{c.version}</td>
178-
<td class="nowrap">{c.date}</td>
179-
<td><a href={c.url} rel="nofollow">Download</a></td>
180-
<td><span class={`badge ${badgeClass}`}>{statusLabel}</span></td>
181-
<td>{c.status || ''}</td>
182-
</tr>
191+
<>
192+
<tr>
193+
<td>
194+
{hasVulns ? (
195+
<button
196+
class="vuln-toggle"
197+
data-target={detailsId}
198+
aria-expanded="false"
199+
aria-controls={detailsId}
200+
>
201+
<span class="toggle-icon">▶</span>
202+
{c.name}
203+
</button>
204+
) : (
205+
c.name
206+
)}
207+
</td>
208+
<td class="nowrap">{c.version}</td>
209+
<td class="nowrap">{c.date}</td>
210+
<td>
211+
<a href={c.url} rel="nofollow">SBOM</a>
212+
{c.advisoriesUrl && (
213+
<>
214+
{' | '}
215+
<a href={c.advisoriesUrl} rel="nofollow">Advisories</a>
216+
</>
217+
)}
218+
</td>
219+
<td><span class={`badge ${badgeClass}`}>{statusLabel}</span></td>
220+
<td>{hasVulns ? (c.status || 'Patch in progress') : ''}</td>
221+
</tr>
222+
{hasVulns && (
223+
<tr id={detailsId} class="vuln-details" style="display: none;">
224+
<td colspan="6">
225+
<div class="vuln-details-content">
226+
<h4>Vulnerability Details</h4>
227+
<div class="vuln-list">
228+
{(() => {
229+
const adv = advisories.get(`${c.name}@${c.version}`);
230+
const vulns = adv?.vulns || [];
231+
232+
if (vulns.length === 0) {
233+
return <p>Vulnerabilities detected but no specific details available.</p>;
234+
}
235+
236+
return (
237+
<div class="vuln-items">
238+
{vulns.slice(0, 10).map((vuln) => (
239+
<div class="vuln-item">
240+
<div class="vuln-header">
241+
<div class="vuln-cve">
242+
{vuln.PrimaryURL ? (
243+
<a href={vuln.PrimaryURL} target="_blank" rel="noopener noreferrer">
244+
<strong>{vuln.VulnerabilityID || 'Unknown CVE'}</strong>
245+
</a>
246+
) : (
247+
<strong>{vuln.VulnerabilityID || 'Unknown CVE'}</strong>
248+
)}
249+
<span class={`severity-badge ${(vuln.Severity || 'unknown').toLowerCase()}`}>
250+
{vuln.Severity || 'Unknown'}
251+
</span>
252+
</div>
253+
{vuln.PkgID && (
254+
<div class="vuln-package">
255+
Package: <code>{vuln.PkgID}</code>
256+
</div>
257+
)}
258+
</div>
259+
{vuln.Title && (
260+
<div class="vuln-title">
261+
{vuln.Title}
262+
</div>
263+
)}
264+
</div>
265+
))}
266+
{vulns.length > 10 && (
267+
<div class="vuln-more">
268+
... and {vulns.length - 10} more vulnerabilities
269+
</div>
270+
)}
271+
</div>
272+
);
273+
})()}
274+
</div>
275+
</div>
276+
</td>
277+
</tr>
278+
)}
279+
</>
183280
);
184281
})}
185282
</tbody>
@@ -285,4 +382,156 @@ const tags = [
285382
color: #555;
286383
border-color: #e5e5e5;
287384
}
288-
</style>
385+
386+
/* Vulnerability toggle button */
387+
.vuln-toggle {
388+
background: none;
389+
border: none;
390+
padding: 0;
391+
font: inherit;
392+
color: inherit;
393+
cursor: pointer;
394+
display: flex;
395+
align-items: center;
396+
gap: 6px;
397+
text-align: left;
398+
}
399+
.vuln-toggle:hover {
400+
color: #0066cc;
401+
}
402+
.toggle-icon {
403+
font-size: 10px;
404+
transition: transform 0.2s ease;
405+
display: inline-block;
406+
width: 12px;
407+
}
408+
.vuln-toggle[aria-expanded="true"] .toggle-icon {
409+
transform: rotate(90deg);
410+
}
411+
412+
/* Vulnerability details row */
413+
.vuln-details {
414+
background: #f8f9fa;
415+
}
416+
.vuln-details-content {
417+
padding: 16px;
418+
border-left: 3px solid #dc3545;
419+
}
420+
.vuln-details-content h4 {
421+
margin: 0 0 12px 0;
422+
font-size: 14px;
423+
font-weight: 600;
424+
color: #333;
425+
}
426+
.vuln-items {
427+
display: flex;
428+
flex-direction: column;
429+
gap: 16px;
430+
}
431+
.vuln-item {
432+
padding: 12px;
433+
background: white;
434+
border: 1px solid #e9ecef;
435+
border-radius: 6px;
436+
}
437+
.vuln-header {
438+
display: flex;
439+
flex-direction: column;
440+
gap: 8px;
441+
margin-bottom: 8px;
442+
}
443+
.vuln-cve {
444+
display: flex;
445+
align-items: center;
446+
gap: 8px;
447+
}
448+
.vuln-cve a {
449+
color: #0066cc;
450+
text-decoration: none;
451+
}
452+
.vuln-cve a:hover {
453+
text-decoration: underline;
454+
}
455+
.vuln-cve strong {
456+
font-family: monospace;
457+
font-size: 13px;
458+
}
459+
.vuln-package {
460+
font-size: 12px;
461+
color: #666;
462+
}
463+
.vuln-package code {
464+
background: #f8f9fa;
465+
padding: 2px 4px;
466+
border-radius: 3px;
467+
font-family: monospace;
468+
font-size: 11px;
469+
}
470+
.vuln-title {
471+
font-size: 13px;
472+
color: #333;
473+
line-height: 1.4;
474+
}
475+
.vuln-more {
476+
padding: 8px 12px;
477+
background: #f8f9fa;
478+
border-radius: 6px;
479+
font-size: 12px;
480+
color: #666;
481+
text-align: center;
482+
font-style: italic;
483+
}
484+
.severity-badge {
485+
font-size: 10px;
486+
padding: 2px 6px;
487+
border-radius: 3px;
488+
font-weight: 600;
489+
text-transform: uppercase;
490+
}
491+
.severity-badge.critical {
492+
background: #dc3545;
493+
color: white;
494+
}
495+
.severity-badge.high {
496+
background: #fd7e14;
497+
color: white;
498+
}
499+
.severity-badge.medium {
500+
background: #ffc107;
501+
color: #000;
502+
}
503+
.severity-badge.low {
504+
background: #28a745;
505+
color: white;
506+
}
507+
.severity-badge.unknown {
508+
background: #6c757d;
509+
color: white;
510+
}
511+
</style>
512+
513+
<script>
514+
// Handle vulnerability details toggle
515+
document.addEventListener('DOMContentLoaded', function() {
516+
const toggleButtons = document.querySelectorAll('.vuln-toggle');
517+
518+
toggleButtons.forEach((button) => {
519+
button.addEventListener('click', function(this: HTMLElement) {
520+
const targetId = this.getAttribute('data-target');
521+
if (!targetId) return;
522+
const targetRow = document.getElementById(targetId);
523+
const isExpanded = this.getAttribute('aria-expanded') === 'true';
524+
525+
if (targetRow) {
526+
if (isExpanded) {
527+
targetRow.style.display = 'none';
528+
this.setAttribute('aria-expanded', 'false');
529+
} else {
530+
targetRow.style.display = 'table-row';
531+
this.setAttribute('aria-expanded', 'true');
532+
}
533+
}
534+
});
535+
});
536+
});
537+
</script>

0 commit comments

Comments
 (0)