// bom-builder.mjs — CycloneDX 1.6 AI-BOM builder // Discovers AI components in a Claude Code project and builds a // CycloneDX-compatible BOM. Zero external dependencies. import { readFile, readdir, access, stat } from 'node:fs/promises'; import { join, basename, extname } from 'node:path'; import { parseFrontmatter } from './yaml-frontmatter.mjs'; // --------------------------------------------------------------------------- // File helpers // --------------------------------------------------------------------------- async function readJson(filePath) { try { return JSON.parse(await readFile(filePath, 'utf-8')); } catch { return null; } } async function readText(filePath) { try { return await readFile(filePath, 'utf-8'); } catch { return null; } } async function fileExists(filePath) { try { await access(filePath); return true; } catch { return false; } } async function listDir(dirPath) { try { return await readdir(dirPath); } catch { return []; } } // --------------------------------------------------------------------------- // Component discovery // --------------------------------------------------------------------------- /** * Extract model references from text (CLAUDE.md, agent frontmatter, settings). * @param {string} text * @returns {string[]} unique model names */ function extractModels(text) { if (!text) return []; const modelPatterns = [ /\b(claude-opus-4-6|claude-sonnet-4-6|claude-haiku-4-5[-\w]*)\b/gi, /\b(opus|sonnet|haiku)\b/gi, /\b(gpt-4o?[-\w]*|gpt-3\.5[-\w]*)\b/gi, /\b(gemini[-\w]*)\b/gi, ]; const found = new Set(); for (const pat of modelPatterns) { for (const match of text.matchAll(pat)) { found.add(match[1].toLowerCase()); } } return [...found]; } /** * Discover all AI components in a project. * @param {string} projectRoot * @returns {Promise} Array of CycloneDX component objects */ export async function discoverComponents(projectRoot) { const components = []; const dependencies = []; // 1. ML Models — from CLAUDE.md, agent frontmatter, settings const modelSources = new Set(); const claudeMd = await readText(join(projectRoot, 'CLAUDE.md')); for (const m of extractModels(claudeMd)) modelSources.add(m); // Agent files const agentDir = join(projectRoot, 'agents'); const agentFiles = (await listDir(agentDir)).filter(f => f.endsWith('.md')); for (const file of agentFiles) { const content = await readText(join(agentDir, file)); if (!content) continue; const fm = parseFrontmatter(content); if (fm?.model) modelSources.add(fm.model.toLowerCase()); for (const m of extractModels(content)) modelSources.add(m); } // Settings files for (const settingsPath of [ join(projectRoot, '.claude', 'settings.json'), join(projectRoot, 'settings.json'), ]) { const settings = await readJson(settingsPath); if (settings) { for (const m of extractModels(JSON.stringify(settings))) modelSources.add(m); } } for (const model of modelSources) { components.push({ type: 'machine-learning-model', name: model, 'bom-ref': `model-${model}`, }); } // 2. MCP Servers — from .mcp.json, .claude/settings.json const mcpSources = [ join(projectRoot, '.mcp.json'), join(projectRoot, '.claude', '.mcp.json'), ]; for (const mcpPath of mcpSources) { const mcpConfig = await readJson(mcpPath); if (!mcpConfig?.mcpServers) continue; for (const [name, config] of Object.entries(mcpConfig.mcpServers)) { components.push({ type: 'framework', name: `mcp-server:${name}`, 'bom-ref': `mcp-${name}`, description: config.command ? `${config.command} ${(config.args || []).join(' ')}`.trim() : undefined, }); } } // 3. Plugins — from global settings enabledPlugins const globalSettings = await readJson(join(projectRoot, '.claude', 'settings.json')); if (globalSettings?.enabledPlugins) { for (const plugin of globalSettings.enabledPlugins) { components.push({ type: 'library', name: `plugin:${basename(plugin)}`, 'bom-ref': `plugin-${basename(plugin)}`, }); } } // 4. Knowledge bases — knowledge/*.md files const knowledgeDir = join(projectRoot, 'knowledge'); const knowledgeFiles = (await listDir(knowledgeDir)).filter(f => f.endsWith('.md') || f.endsWith('.json')); for (const file of knowledgeFiles) { components.push({ type: 'data', name: `knowledge:${file}`, 'bom-ref': `knowledge-${file}`, }); } // 5. Hooks — from hooks/hooks.json const hooksJson = await readJson(join(projectRoot, 'hooks', 'hooks.json')); if (hooksJson?.hooks) { for (const [event, entries] of Object.entries(hooksJson.hooks)) { for (const entry of (Array.isArray(entries) ? entries : [entries])) { const hooks = entry.hooks || []; for (const hook of hooks) { if (hook.command) { const hookName = basename(hook.command.split('/').pop().replace(/\$\{.*?\}/g, '')); components.push({ type: 'framework', name: `hook:${hookName}`, 'bom-ref': `hook-${hookName}-${event}`, description: `${event} hook${entry.matcher ? ` (matcher: ${entry.matcher})` : ''}`, }); } } } } } return components; } /** * Build a CycloneDX 1.6 AI-BOM from discovered components. * @param {object[]} components - From discoverComponents() * @param {object} [projectMeta] - { name, version } * @returns {object} CycloneDX 1.6 JSON */ export function buildAIBOM(components, projectMeta = {}) { return { bomFormat: 'CycloneDX', specVersion: '1.6', version: 1, metadata: { timestamp: new Date().toISOString(), tools: [{ vendor: 'llm-security', name: 'ai-bom-generator', version: '6.0.0', }], component: { type: 'application', name: projectMeta.name || 'unknown', version: projectMeta.version || '0.0.0', }, }, components, dependencies: [], }; }