@@ -12,15 +12,14 @@ import FlexibleSection from "../components/FlexibleSection.astro";
1212// Per-component metadata (id, name, version, repo) to build exact URLs
1313interface ComponentDef { id: ' defguard' | ' defguard-client' | ' defguard-mobile' | ' defguard-proxy' | ' defguard-gateway' ; name: string ; version: string ; repo: string ; status: string }
1414const 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
2222const buildReleaseTag = (version : string ): string => ` v${version } ` ;
23- // const basePath = 'sboms';
2423const basePath = ' https://github.com/DefGuard' ;
2524const buildBaseUrl = (repo : string , version : string ): string => ` ${basePath }/${repo }/releases/download/${buildReleaseTag (version )}/ ` ;
2625const 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
4241interface 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+ }
4449interface AdvisoryResult { Vulnerabilities? : Vulnerability [] }
4550interface 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
6671const 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