1+ ---
2+ // File suggestion: src/pages/security/sbom.astro
3+ // This page is designed to be data‑driven. Replace the mock `components` and
4+ // `vulnReports` with content generated in CI (e.g., from Trivy + SBOM artifacts).
5+ // You can also load JSON via `Astro.fetchContent` or import from `src/content`.
6+
7+ import Navigation from " ../components/base/Navigation.astro" ;
8+ import ProductLayout from " ../layouts/ProductLayout.astro" ;
9+ import FlexibleSection from " ../components/FlexibleSection.astro" ;
10+ // Fetch SBOM/advisories directly from GitHub Releases
11+
12+ // Per-component metadata (id, name, version, repo) to build exact URLs
13+ interface ComponentDef { id: ' defguard' | ' defguard-client' | ' defguard-mobile' | ' defguard-proxy' | ' defguard-gateway' ; name: string ; version: string ; repo: string ; status: string }
14+ const COMPONENTS: ComponentDef [] = [
15+ { id: ' defguard' , name: ' Core' , version: ' 1.5.1' , repo: ' defguard' , status: ' Fix in testing' },
16+ { id: ' defguard-proxy' , name: ' Proxy' , version: ' 1.5.1' , repo: ' proxy' , status: ' ' },
17+ { id: ' defguard-gateway' , name: ' Gateway' , version: ' 1.5.1' , repo: ' gateway' , status: ' ' },
18+ { 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: ' ' },
20+ ];
21+
22+ const buildReleaseTag = (version : string ): string => ` v${version } ` ;
23+ // const basePath = 'sboms';
24+ const basePath = ' https://github.com/DefGuard' ;
25+ const buildBaseUrl = (repo : string , version : string ): string => ` ${basePath }/${repo }/releases/download/${buildReleaseTag (version )}/ ` ;
26+ const buildSbomUrl = (c : ComponentDef ): string => ` ${buildBaseUrl (c .repo , c .version )}${c .id }-${c .version }.sbom.json ` ;
27+ const buildAdvisoriesUrl = (c : ComponentDef ): string => ` ${buildBaseUrl (c .repo , c .version )}${c .id }-${c .version }.advisories.json ` ;
28+
29+ const severityOrder: string [] = [' critical' ,' high' ,' medium' ,' low' ,' unknown' ];
30+ const pickHighestSeverity = (items : string []): string => {
31+ let highest: string = ' unknown' ;
32+ for (const s of items ) {
33+ const normalized = (s || ' ' ).toLowerCase ();
34+ const idx = severityOrder .indexOf (normalized );
35+ const cur = severityOrder .indexOf (highest );
36+ if (idx !== - 1 && (cur === - 1 || idx < cur )) highest = normalized ;
37+ }
38+ return highest ;
39+ };
40+
41+ // Group by component/version
42+ interface SbomEntry { name: string ; version: string ; url: string ; status: string }
43+ interface Vulnerability { Severity? : string ; VulnerabilityID? : string }
44+ interface AdvisoryResult { Vulnerabilities? : Vulnerability [] }
45+ interface AdvisoryFile { CreatedAt? : string ; Metadata? : { CreatedAt? : string }; Results? : AdvisoryResult [] }
46+
47+ const sboms = new Map <string , SbomEntry >();
48+ const advisories = new Map <string , { createdAt: string | null ; vulns: Vulnerability [] }>();
49+
50+ // Build entries and fetch advisories from GitHub
51+ await Promise .all (COMPONENTS .map (async (c ) => {
52+ const idKey = ` ${c .name }@${c .version } ` ;
53+ sboms .set (idKey , { name: c .name , version: c .version , url: buildSbomUrl (c ), status: c .status });
54+ try {
55+ const res = await fetch (buildAdvisoriesUrl (c ));
56+ if (res .ok ) {
57+ const json = (await res .json ()) as AdvisoryFile ;
58+ const createdAt = json .CreatedAt || json .Metadata ?.CreatedAt || null ;
59+ const vulns = (json .Results || []).flatMap ((r : AdvisoryResult ) => (r .Vulnerabilities || []));
60+ advisories .set (idKey , { createdAt , vulns });
61+ }
62+ } catch {}
63+ }));
64+
65+ // Build components array for table
66+ const components = Array .from (sboms .values ()).map ((c : SbomEntry ) => {
67+ const adv = advisories .get (` ${c .name }@${c .version } ` );
68+ return {
69+ name: c .name ,
70+ format: ' JSON' ,
71+ version: c .version ,
72+ date: adv ?.createdAt ? String (adv .createdAt ).slice (0 , 10 ) : ' —' ,
73+ url: c .url ,
74+ status: c .status ,
75+ };
76+ });
77+
78+ // Build vulnerability reports aligned with components
79+ interface VulnReport { component: string ; version: string ; status: ' ok' | ' issues' ; severity: string ; cves: string []; action: string }
80+ const vulnReports: VulnReport [] = components .map ((c ) => {
81+ const adv = advisories .get (` ${c .name }@${c .version } ` );
82+ const vulns = adv ?.vulns || [];
83+ if (vulns .length === 0 ) {
84+ return { component: c .name , version: c .version , status: ' ok' , severity: ' None' , cves: [], action: ' —' };
85+ }
86+ const highest = pickHighestSeverity (vulns .map ((v : Vulnerability ) => v .Severity || ' unknown' ));
87+ // Normalize label case
88+ const severityLabel = highest .charAt (0 ).toUpperCase () + highest .slice (1 );
89+ const cves = vulns .map ((v : Vulnerability ) => v .VulnerabilityID ).filter (Boolean ).slice (0 , 5 ) as string [];
90+ return { component: c .name , version: c .version , status: ' issues' , severity: severityLabel , cves , action: ' —' };
91+ });
92+
93+ // Note: table shows per-component status; page-level aggregate not used currently.
94+ const title = " defguard - Zero-Trust WireGuard® 2FA/MFA VPN" ;
95+ const featuredImage =
96+ " github.com/DefGuard/defguard.github.io/raw/main/public/images/product/core/hero-image.png" ;
97+ const imageWidth = " 1920" ;
98+ const imageHeight = " 1080" ;
99+ const url = " https://defguard.net/sbom" ;
100+ const tags = [
101+ " defguard" ,
102+ " security" ,
103+ " sbom" ,
104+ " sca" ,
105+ " vulnerability" ,
106+ " supply chain" ,
107+ " transparency" ,
108+ ];
109+ ---
110+
111+ <ProductLayout
112+ title ={ title }
113+ featuredImage ={ featuredImage }
114+ imageWidth ={ imageWidth }
115+ imageHeight ={ imageHeight }
116+ url ={ url }
117+ tags ={ tags }
118+ >
119+ <Navigation activeSlug =" /security/" />
120+
121+ <main id =" home-page" >
122+ <FlexibleSection leftRatio ={ 1 } title =" What is SBOM?" theme =" light" >
123+ <div slot =" left" class =" sbom-intro" >
124+ <p >
125+ A <strong >Software Bill of Materials (SBOM)</strong > is a structured inventory of all components that make up
126+ a piece of software — including third‑party libraries, packages, versions, and their relationships.
127+ SBOMs help organizations understand what is inside their software, evaluate exposure to known
128+ vulnerabilities, and meet supply‑chain security and compliance requirements.
129+ </p >
130+ <p >
131+ We publish SBOMs because <strong >transparency and security</strong > are core to Defguard. Making our
132+ dependency information public lets customers and auditors independently <strong >verify what we ship</strong >,
133+ continuously <strong >assess risk</strong > against public CVE databases, and integrate our artifacts into their
134+ own security tooling and compliance workflows.
135+ </p >
136+ <p >
137+ SBOMs also help us <strong >respond faster</strong > to newly disclosed issues: we track and scan dependencies
138+ after each release, prioritize remediation, and communicate status openly. This practice aligns with
139+ ISO 27001 controls and demonstrates our commitment to a secure software supply chain.
140+ </p >
141+ </div >
142+ </FlexibleSection >
143+
144+
145+ <FlexibleSection leftRatio ={ 1 } title =" SBOM file list with vulnerability status" theme =" light" >
146+ <div slot =" left" class =" sbom-filelist" >
147+ <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.
151+ </p >
152+ <div class =" content-measure" >
153+ <table class =" sbom-table" role =" table" aria-label =" SBOM list with vulnerability status" >
154+ <thead >
155+ <tr >
156+ <th >Component</th >
157+ <th >Version</th >
158+ <th >Date checked</th >
159+ <th >SBOM link</th >
160+ <th >Vulnerability status</th >
161+ <th >Status</th >
162+ </tr >
163+ </thead >
164+ <tbody >
165+ { components .map ((c ) => {
166+ const report = vulnReports .find ((r ) => r .component === c .name && r .version === c .version );
167+ const statusLabel = report
168+ ? (report .status === ' ok' ? ' No vulnerabilities' : ` ${report .severity } vulnerabilities ` )
169+ : ' —' ;
170+ const severity = (report ?.severity ?? ' ' ).toLowerCase ();
171+ const badgeClass = report
172+ ? (report .status === ' ok' ? ' ok' : (severity || ' issues' ))
173+ : ' na' ;
174+ 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 >
183+ );
184+ })}
185+ </tbody >
186+ </table >
187+ </div >
188+ </div >
189+ </FlexibleSection >
190+
191+ </main >
192+ </ProductLayout >
193+
194+ <style >
195+ .content-measure {
196+ max-width: 100%;
197+ }
198+ .content-measure table {
199+ width: 100%;
200+ }
201+ /* spacing between paragraphs in the SBOM intro section */
202+ .sbom-intro p + p {
203+ margin-top: 1rem;
204+ }
205+ /* spacing between intro paragraph and table in SBOM file list */
206+ .sbom-filelist p {
207+ margin-bottom: 1rem;
208+ }
209+ /* SBOM table styles */
210+ .sbom-table {
211+ border-collapse: separate;
212+ border-spacing: 0;
213+ width: 100%;
214+ background: #fff;
215+ border: 1px solid rgba(0,0,0,0.08);
216+ border-radius: 8px;
217+ overflow: hidden;
218+ }
219+ .sbom-table thead th {
220+ text-align: left;
221+ font-weight: 600;
222+ background: #fafafa;
223+ color: #111;
224+ padding: 12px 14px;
225+ border-bottom: 1px solid rgba(0,0,0,0.06);
226+ white-space: nowrap;
227+ }
228+ .sbom-table tbody td {
229+ padding: 12px 14px;
230+ border-bottom: 1px solid rgba(0,0,0,0.06);
231+ vertical-align: middle;
232+ }
233+ .sbom-table tbody tr:hover {
234+ background: rgba(0,0,0,0.02);
235+ }
236+ .sbom-table .nowrap {
237+ white-space: nowrap;
238+ }
239+ .sbom-table a {
240+ color: inherit;
241+ text-decoration: underline;
242+ }
243+ .badge {
244+ display: inline-block;
245+ padding: 2px 8px;
246+ border-radius: 999px;
247+ font-size: 12px;
248+ line-height: 1.6;
249+ font-weight: 600;
250+ border: 1px solid transparent;
251+ }
252+ .badge.ok {
253+ background: #e8f7ef;
254+ color: #18794e;
255+ border-color: #b7ebcd;
256+ }
257+ .badge.issues {
258+ background: #fff1f0;
259+ color: #a8071a;
260+ border-color: #ffccc7;
261+ }
262+ /* severity-specific badges */
263+ .badge.low {
264+ background: #f0faf4;
265+ color: #0e7a3d;
266+ border-color: #bfead1;
267+ }
268+ .badge.medium {
269+ background: #fffbe6;
270+ color: #ad8b00;
271+ border-color: #ffe58f;
272+ }
273+ .badge.high {
274+ background: #fff1f0;
275+ color: #a8071a;
276+ border-color: #ffccc7;
277+ }
278+ .badge.critical {
279+ background: #fff1f0;
280+ color: #a8071a;
281+ border-color: #ffccc7;
282+ }
283+ .badge.na {
284+ background: #f5f5f5;
285+ color: #555;
286+ border-color: #e5e5e5;
287+ }
288+ </style >
0 commit comments