New bom-builder.mjs discovers AI components (models, MCP servers, plugins, knowledge files, hooks) and builds CycloneDX 1.6 JSON BOMs. CLI entry point: node scanners/ai-bom-generator.mjs <target>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
6 KiB
JavaScript
195 lines
6 KiB
JavaScript
// 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<object[]>} 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: [],
|
|
};
|
|
}
|