From 0f5795c1b767be2d4fcbd257e77b573661bae520 Mon Sep 17 00:00:00 2001 From: Jerry Gamblin Date: Fri, 3 Apr 2026 17:29:50 -0500 Subject: [PATCH] docs: add SBOM design spec and implementation plan for issue #53 Adds CycloneDX 1.6 and SPDX 2.3 SBOM planning documents covering runtime and dev dependencies for the cveClient project. Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-04-02-sbom-implementation.md | 1635 +++++++++++++++++ .../specs/2026-04-02-sbom-design.md | 278 +++ 2 files changed, 1913 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-02-sbom-implementation.md create mode 100644 docs/superpowers/specs/2026-04-02-sbom-design.md diff --git a/docs/superpowers/plans/2026-04-02-sbom-implementation.md b/docs/superpowers/plans/2026-04-02-sbom-implementation.md new file mode 100644 index 0000000..029b0f2 --- /dev/null +++ b/docs/superpowers/plans/2026-04-02-sbom-implementation.md @@ -0,0 +1,1635 @@ +# SBOM Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Generate comprehensive SBOMs (CycloneDX 1.6, SPDX 2.3 JSON, SPDX 2.3 tag-value, and Markdown) for all cveClient runtime and dev components, automated via GitHub Actions. + +**Architecture:** A modular Node.js generator under `scripts/sbom/` extracts component data from project source files, then serializes to four output formats in `docs/sbom/`. A GitHub Action runs the generator on relevant file changes and opens a PR via `gh`. + +**Tech Stack:** Node.js (built-in `fs`, `path`, `crypto`), Vitest (testing), `npx ajv-cli` (CI validation), GitHub Actions + `gh` CLI (automation). + +--- + +## File Structure + +``` +scripts/ + generate-sbom.mjs # Main entry point - orchestrates extraction and generation + sbom/ + extract.mjs # Extracts component data from project files + cyclonedx.mjs # Generates CycloneDX 1.6 JSON + spdx.mjs # Generates SPDX 2.3 JSON and tag-value + markdown.mjs # Generates SBOM.md summary + +tests/ + sbom/ + extract.test.js # Tests for data extraction + cyclonedx.test.js # Tests for CycloneDX output + spdx.test.js # Tests for SPDX JSON and tag-value output + markdown.test.js # Tests for Markdown output + +docs/sbom/ # Output directory (generated files) + cyclonedx-runtime.json + cyclonedx-dev.json + spdx-runtime.json + spdx-dev.json + spdx-runtime.spdx + spdx-dev.spdx + SBOM.md + +.github/workflows/ + generate-sbom.yml # GitHub Action workflow +``` + +**Why `.mjs` for scripts:** The project's `package.json` has no `"type": "module"`, so `.mjs` enables ESM imports without affecting existing code. Test files stay `.js` since vitest handles ESM natively. + +--- + +### Task 1: Component Data Extraction Module + +**Files:** + +- Create: `scripts/sbom/extract.mjs` +- Create: `tests/sbom/extract.test.js` + +This is the core module - it reads project files and returns structured component inventories. + +- [ ] **Step 1: Write failing tests for HTML CDN extraction** + +Create `tests/sbom/extract.test.js`: + +```js +import { describe, it, expect } from "vitest"; +import { extractCdnDeps } from "../../scripts/sbom/extract.mjs"; + +describe("extractCdnDeps", () => { + it("extracts script tags with integrity hashes", () => { + const html = ` + + `; + const deps = extractCdnDeps(html); + expect(deps).toHaveLength(1); + expect(deps[0]).toMatchObject({ + name: "jquery", + version: "3.5.1", + url: "https://code.jquery.com/jquery-3.5.1.min.js", + integrity: + "sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2", + type: "script", + }); + }); + + it("extracts link tags with integrity hashes", () => { + const html = ` + + `; + const deps = extractCdnDeps(html); + expect(deps).toHaveLength(1); + expect(deps[0]).toMatchObject({ + name: "bootstrap", + version: "4.3.1", + type: "stylesheet", + }); + }); + + it("extracts multiple deps from full HTML", () => { + const html = ` + + + + + + + `; + const deps = extractCdnDeps(html); + expect(deps).toHaveLength(6); + const names = deps.map((d) => d.name); + expect(names).toContain("jquery"); + expect(names).toContain("popper.js"); + expect(names).toContain("bootstrap"); + expect(names).toContain("bootstrap-table"); + }); + + it("skips local scripts without integrity", () => { + const html = ` + + + `; + const deps = extractCdnDeps(html); + expect(deps).toHaveLength(1); + expect(deps[0].name).toBe("jquery"); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `npx vitest run tests/sbom/extract.test.js` +Expected: FAIL - module not found + +- [ ] **Step 3: Write failing tests for source file version extraction** + +Append to `tests/sbom/extract.test.js`: + +```js +import { extractSourceVersions } from "../../scripts/sbom/extract.mjs"; + +describe("extractSourceVersions", () => { + it("extracts this._version pattern", () => { + const content = `class Foo {\n constructor() {\n this._version = "1.0.12";\n }\n}`; + const version = extractSourceVersions.parseVersion(content); + expect(version).toBe("1.0.12"); + }); + + it("extracts const _version pattern", () => { + const content = `const _version = "1.0.25";`; + const version = extractSourceVersions.parseVersion(content); + expect(version).toBe("1.0.25"); + }); + + it("extracts const name_version pattern", () => { + const content = `const encrypt_storage_version = "1.1.15";`; + const version = extractSourceVersions.parseVersion(content); + expect(version).toBe("1.1.15"); + }); +}); +``` + +- [ ] **Step 4: Write failing tests for vendored dependency extraction** + +Append to `tests/sbom/extract.test.js`: + +```js +import { extractVendoredVersion } from "../../scripts/sbom/extract.mjs"; + +describe("extractVendoredVersion", () => { + it("extracts SweetAlert2 version from header comment", () => { + const content = `/*!\n* sweetalert2 v11.26.24\n* Released under the MIT License.\n*/`; + const result = extractVendoredVersion.parseSweetalert(content); + expect(result).toMatchObject({ + name: "sweetalert2", + version: "11.26.24", + license: "MIT", + }); + }); + + it("extracts Ace Editor version from source", () => { + const content = `version="1.4.12"}),ace.define("ace/mouse"`; + const result = extractVendoredVersion.parseAce(content); + expect(result).toMatchObject({ + name: "ace-editor", + version: "1.4.12", + license: "Apache-2.0", + }); + }); +}); +``` + +- [ ] **Step 5: Write failing tests for dev dependency extraction** + +Append to `tests/sbom/extract.test.js`: + +```js +import { extractDevDeps } from "../../scripts/sbom/extract.mjs"; + +describe("extractDevDeps", () => { + it("extracts npm dev dependencies from package.json and lock", () => { + const pkg = { + devDependencies: { vitest: "^3.1.0", jsdom: "^26.1.0" }, + }; + const lock = { + packages: { + "node_modules/vitest": { version: "3.2.4", license: "MIT" }, + "node_modules/jsdom": { version: "26.1.0", license: "MIT" }, + "node_modules/chai": { version: "5.2.0", license: "MIT" }, + }, + }; + const result = extractDevDeps.fromNpm(pkg, lock); + // Direct deps + expect(result.direct).toHaveLength(2); + expect(result.direct[0]).toMatchObject({ + name: "vitest", + version: "3.2.4", + }); + // Transitive deps + expect(result.transitive.length).toBeGreaterThan(0); + expect(result.transitive[0]).toMatchObject({ + name: "chai", + version: "5.2.0", + }); + }); + + it("extracts GitHub Actions from workflow YAML", () => { + const yaml = ` +name: Tests +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm test +`; + const actions = extractDevDeps.fromWorkflowYaml(yaml); + expect(actions).toHaveLength(2); + expect(actions[0]).toMatchObject({ + name: "actions/checkout", + version: "v4", + }); + expect(actions[1]).toMatchObject({ + name: "actions/setup-node", + version: "v4", + }); + }); +}); +``` + +- [ ] **Step 6: Implement `scripts/sbom/extract.mjs`** + +Create `scripts/sbom/extract.mjs`: + +```js +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; + +/** + * Extract CDN dependencies from HTML string. + * Finds