Skip to content

Commit e1c8261

Browse files
authored
Added SBOM page (#100)
1 parent c10c8e3 commit e1c8261

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

src/pages/sbom.astro

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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

Comments
 (0)