Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ CLAUDE.md
# Generated printable checklists
docs/public/printable/

.direnv
.direnv

# Avoid submitting certs data to repo
cert-data.json
187 changes: 187 additions & 0 deletions components/cert/ExportAllCerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { useState } from "react";
import ExcelJS from "exceljs";
import { ControlData, ControlState, Section } from "./types";
import "./control.css";

interface CertDefinition {
name: string;
label: string;
sections: Section[];
}

const stateToText: Record<ControlState, string> = {
no: "No",
yes: "Yes",
partial: "Partial",
na: "N/A",
};

function getStoredData(certName: string): Record<string, ControlData> {
try {
const saved = localStorage.getItem(`certList-${certName}`);
return saved ? JSON.parse(saved) : {};
} catch {
return {};
}
}

function addCertSheet(
workbook: ExcelJS.Workbook,
label: string,
sections: Section[],
controlData: Record<string, ControlData>,
) {
const sheetName = label.length > 31 ? label.slice(0, 31) : label;
const worksheet = workbook.addWorksheet(sheetName);

worksheet.columns = [
{ header: "Section", key: "section", width: 12 },
{ header: "Control ID", key: "id", width: 15 },
{ header: "Question", key: "question", width: 80 },
{ header: "Baseline Requirements", key: "baselines", width: 60 },
{ header: "Response", key: "response", width: 12 },
{ header: "N/A Justification", key: "justification", width: 40 },
{ header: "Evidence / Notes", key: "notes", width: 50 },
];

const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: "FFFFFFFF" } };
headerRow.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FF4339DB" },
};
headerRow.alignment = { vertical: "middle", horizontal: "left" };
headerRow.height = 25;

let currentRow = 2;

sections.forEach((section, sectionIndex) => {
const sectionHeaderRow = worksheet.getRow(currentRow);
worksheet.mergeCells(currentRow, 1, currentRow, 7);

const sectionCell = sectionHeaderRow.getCell(1);
sectionCell.value = `Section ${sectionIndex + 1}${section.title}`;
sectionCell.font = { bold: true, size: 12 };
sectionCell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFF3F4F6" },
};
sectionCell.alignment = { vertical: "middle", horizontal: "left" };
sectionHeaderRow.height = 22;
currentRow++;

section.controls.forEach((control) => {
const data = controlData[control.id] || { state: "no", justification: "", evidence: "" };
const baselinesText = control.baselines?.length
? control.baselines.map((b, i) => `${i + 1}. ${b}`).join("\n")
: "";

const row = worksheet.addRow({
section: `Section ${sectionIndex + 1}`,
id: control.id,
question: control.description,
baselines: baselinesText,
response: stateToText[data.state],
justification: data.justification,
notes: data.evidence,
});

const rowNumber = row.number;

worksheet.getCell(`E${rowNumber}`).dataValidation = {
type: "list",
allowBlank: false,
formulae: ['"No,Yes,Partial,N/A"'],
showErrorMessage: true,
errorTitle: "Invalid Response",
error: "Please select: No, Yes, Partial, or N/A",
};
worksheet.getCell(`E${rowNumber}`).alignment = { vertical: "middle", horizontal: "left" };

for (const col of ["C", "D", "F", "G"]) {
worksheet.getCell(`${col}${rowNumber}`).alignment = { wrapText: true, vertical: "top", horizontal: "left" };
}
for (const col of ["A", "B"]) {
worksheet.getCell(`${col}${rowNumber}`).alignment = { vertical: "middle", horizontal: "left" };
}

row.eachCell((cell) => {
cell.border = {
top: { style: "thin", color: { argb: "FFD1D5DB" } },
left: { style: "thin", color: { argb: "FFD1D5DB" } },
bottom: { style: "thin", color: { argb: "FFD1D5DB" } },
right: { style: "thin", color: { argb: "FFD1D5DB" } },
};
});
currentRow++;
});
});

worksheet.autoFilter = { from: "A1", to: `G${worksheet.rowCount}` };
worksheet.views = [{ state: "frozen", xSplit: 0, ySplit: 1, topLeftCell: "A2", activeCell: "A2" }];
}

export function ExportAllCerts() {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleExportAll = async () => {
setExporting(true);
setError(null);

try {
// Fetch cert data generated at build time
const resp = await fetch("/cert-data.json");
if (!resp.ok) throw new Error("Failed to load certification data");
const certs: CertDefinition[] = await resp.json();

const workbook = new ExcelJS.Workbook();
workbook.creator = "SEAL Certifications";
workbook.created = new Date();

for (const cert of certs) {
const controlData = getStoredData(cert.name);
addCertSheet(workbook, cert.label, cert.sections, controlData);
}

const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const timestamp = new Date().toISOString().replace(/T/, "-").replace(/:/g, "-").split(".")[0];
a.download = `seal-certifications-all-${timestamp}.xlsx`;
a.click();
URL.revokeObjectURL(url);
} catch (err) {
setError(`Export failed: ${err instanceof Error ? err.message : "Unknown error"}`);
} finally {
setExporting(false);
}
};

return (
<div style={{ margin: "1rem 0" }}>
<button
type="button"
onClick={handleExportAll}
disabled={exporting}
className="cert-action-btn cert-export-btn"
style={{ fontSize: "0.95rem", padding: "0.5rem 1.25rem" }}
>
{exporting ? "Exporting…" : "Export All Certifications (XLSX)"}
</button>
{error && (
<div className="cert-error-message" role="alert" style={{ marginTop: "0.5rem" }}>
{error}
</div>
)}
</div>
);
}

ExportAllCerts.displayName = "ExportAllCerts";
1 change: 1 addition & 0 deletions components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { ContributeFooter } from './footer/ContributeFooter'
export { Contributors } from './contributors/Contributors'
export { BenchmarkList } from './benchmark/Benchmark'
export { CertList } from './cert/CertList'
export { ExportAllCerts } from './cert/ExportAllCerts'
export type { Control, Section, CertListProps, ControlState, ControlData } from './cert/types'
export { default as MermaidRenderer } from './mermaid/MermaidRenderer';
export * from './shared/tagColors'
Expand Down
4 changes: 3 additions & 1 deletion docs/pages/certs/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- Certifications
---

import { TagList, AttributionList, TagProvider, TagFilter, ContributeFooter } from '../../../components'
import { TagList, AttributionList, TagProvider, TagFilter, ContributeFooter, ExportAllCerts } from '../../../components'

<TagProvider>
<TagFilter />
Expand Down Expand Up @@ -51,6 +51,8 @@

## Certifications Being Developed

<ExportAllCerts />

Check failure on line 54 in docs/pages/certs/overview.mdx

View workflow job for this annotation

GitHub Actions / lint

Inline HTML

docs/pages/certs/overview.mdx:54:1 MD033/no-inline-html Inline HTML [Element: ExportAllCerts] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md033.md

- **[DevOps & Infrastructure](/certs/sfc-devops-infrastructure.mdx)** - Development environments, CI/CD pipelines,
infrastructure security, supply chain
- **[DNS Security](/certs/sfc-dns-registrar.mdx)** - Domain management, DNS configurations, registrar protection
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"docs:dev": "pnpm run generate-tags && pnpm run generate-indexes && pnpm run mermaid-wrapper && vocs dev --host 0.0.0.0 --port 5173",
"docs:build": "pnpm run generate-tags && pnpm run generate-indexes && pnpm run mermaid-wrapper && pnpm run generate-printables && vocs build",
"docs:build": "pnpm run generate-tags && pnpm run generate-indexes && pnpm run mermaid-wrapper && pnpm run generate-printables && pnpm run generate-cert-data && vocs build",
"postdocs:build": "node utils/searchbar-indexing.js && node utils/sitemap-generator.js",
"docs:preview": "vocs preview",
"generate-tags": "node utils/tags-fetcher.js",
"mermaid-wrapper": "node utils/mermaid-block-wrapper.js",
"generate-indexes": "node utils/generate-folder-indexes.js",
"generate-printables": "node utils/generate-printable-checklists.js"
"generate-printables": "node utils/generate-printable-checklists.js",
"generate-cert-data": "node utils/generate-cert-data.js"
},
"keywords": [],
"author": "",
Expand Down
72 changes: 72 additions & 0 deletions utils/generate-cert-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Generate Cert Data JSON
*
* Extracts all SFC certification section/control data from MDX frontmatter
* and writes a JSON file for use by the ExportAllCerts component.
*
* Usage: node utils/generate-cert-data.js
*/

const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');

const CERTS_DIR = path.join(__dirname, '../docs/pages/certs');
const OUTPUT_PATH = path.join(__dirname, '../docs/public/cert-data.json');

const CERT_ORDER = [
{ file: 'sfc-devops-infrastructure.mdx', label: 'DevOps & Infrastructure' },
{ file: 'sfc-dns-registrar.mdx', label: 'DNS Registrar' },
{ file: 'sfc-incident-response.mdx', label: 'Incident Response' },
{ file: 'sfc-multisig-ops.mdx', label: 'Multisig Operations' },
{ file: 'sfc-treasury-ops.mdx', label: 'Treasury Operations' },
{ file: 'sfc-workspace-security.mdx', label: 'Workspace Security' },
];

function main() {
console.log('Generating cert data JSON...\n');

const certs = [];

for (const { file, label } of CERT_ORDER) {
const filePath = path.join(CERTS_DIR, file);
if (!fs.existsSync(filePath)) {
console.log(` Skipping ${file} - not found`);
continue;
}

const content = fs.readFileSync(filePath, 'utf8');
const { data } = matter(content);

if (!data.cert || !Array.isArray(data.cert)) {
console.log(` Skipping ${file} - no cert data`);
continue;
}

const name = file.replace('.mdx', '');
const controlCount = data.cert.reduce((sum, s) => sum + (s.controls?.length || 0), 0);

certs.push({
name,
label,
sections: data.cert,
});

console.log(` ✓ ${name} (${data.cert.length} sections, ${controlCount} controls)`);
}

// Ensure output directory exists
const outputDir = path.dirname(OUTPUT_PATH);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

fs.writeFileSync(OUTPUT_PATH, JSON.stringify(certs, null, 2));

const totalControls = certs.reduce((sum, c) =>
sum + c.sections.reduce((s, sec) => s + (sec.controls?.length || 0), 0), 0
);
console.log(`\n✅ Generated cert-data.json (${certs.length} certs, ${totalControls} total controls)`);
}

main();
Loading