@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
683 lines (660 loc) • 18.6 kB
JavaScript
import { readFileSync } from "node:fs";
import { basename, dirname, extname } from "node:path";
import { parse as loadYaml } from "yaml";
import { parseJsonLike } from "./jsonLike.js";
import {
credentialIndicatorsForText,
sanitizeMcpRefToken,
} from "./mcpDiscovery.js";
import { sanitizeBomPropertyValue } from "./propertySanitizer.js";
import { scanTextForHiddenUnicode } from "./unicodeScan.js";
const COMMUNITY_AI_PATTERNS = [
"opencode.json",
"**/opencode.json",
"opencode.jsonc",
"**/opencode.jsonc",
".opencode/agents/*.md",
"**/.opencode/agents/*.md",
".nanocoder/agents/*.md",
"**/.nanocoder/agents/*.md",
".opencode/{tool,tools}/*.{js,ts,mjs,cjs}",
"**/.opencode/{tool,tools}/*.{js,ts,mjs,cjs}",
".opencode/{skill,skills}/*/SKILL.md",
"**/.opencode/{skill,skills}/*/SKILL.md",
".claude/skills/*/SKILL.md",
"**/.claude/skills/*/SKILL.md",
".agents/skills/*/SKILL.md",
"**/.agents/skills/*/SKILL.md",
".nanocoder/commands/**/*.md",
"**/.nanocoder/commands/**/*.md",
".nanocoder/skills/**/*.md",
"**/.nanocoder/skills/**/*.md",
"langgraph.json",
"**/langgraph.json",
"agents.py",
"**/agents.py",
"tasks.py",
"**/tasks.py",
"config/agents.yaml",
"**/config/agents.yaml",
"config/tasks.yaml",
"**/config/tasks.yaml",
"config/tools.yaml",
"**/config/tools.yaml",
];
function addUniqueProperty(properties, name, value) {
const sanitizedValue = sanitizeBomPropertyValue(name, value);
if (
sanitizedValue === undefined ||
sanitizedValue === null ||
sanitizedValue === ""
) {
return;
}
if (
properties.some(
(prop) => prop.name === name && prop.value === String(sanitizedValue),
)
) {
return;
}
properties.push({ name, value: String(sanitizedValue) });
}
function normalizeFilePath(filePath) {
return filePath.replaceAll("\\", "/");
}
function syntaxForFile(filePath) {
const extension = extname(filePath).toLowerCase();
if ([".json", ".jsonc"].includes(extension)) {
return "json";
}
if ([".yaml", ".yml"].includes(extension)) {
return "yaml";
}
if ([".md", ".markdown"].includes(extension)) {
return "markdown";
}
return "text";
}
function parseFrontmatter(raw) {
const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/u);
if (!match) {
return { body: raw.trim(), metadata: {} };
}
let metadata = {};
try {
metadata = loadYaml(match[1]) || {};
} catch {
metadata = {};
}
return { body: match[2].trim(), metadata };
}
function frameworkForFile(filePath, raw = "") {
const lowerPath = normalizeFilePath(filePath).toLowerCase();
if (
lowerPath.includes("/.opencode/") ||
lowerPath.endsWith("opencode.json") ||
lowerPath.endsWith("opencode.jsonc")
) {
return "opencode";
}
if (lowerPath.includes("/.nanocoder/") || lowerPath.endsWith(".mcp.json")) {
return "nanocoder";
}
if (lowerPath.endsWith("langgraph.json")) {
return "langgraph";
}
if (
lowerPath.endsWith("agents.py") ||
lowerPath.endsWith("tasks.py") ||
lowerPath.includes("/config/agents.yaml") ||
lowerPath.includes("/config/tasks.yaml") ||
lowerPath.includes("/config/tools.yaml") ||
/\bcrewai\b/u.test(raw)
) {
return "crewai";
}
if (lowerPath.includes("/.claude/skills/")) {
return "claude-compatible";
}
if (lowerPath.includes("/.agents/skills/")) {
return "agent-compatible";
}
return "community-ai";
}
function baseProperties(filePath, framework, kind) {
return [
{ name: "SrcFile", value: filePath },
{ name: "cdx:file:kind", value: kind },
{ name: "cdx:agent:inventorySource", value: "community-config" },
{ name: "cdx:agent:framework", value: framework },
];
}
function createComponent(
filePath,
framework,
kind,
name,
type = "application",
) {
return {
"bom-ref": `urn:component:${framework}:${kind}:${sanitizeMcpRefToken(name)}:${sanitizeMcpRefToken(basename(filePath))}`,
name,
properties: baseProperties(filePath, framework, kind),
type,
version: type === "file" ? undefined : "configured",
};
}
function createService(filePath, framework, kind, name, properties = []) {
return {
"bom-ref": `urn:service:${framework}:${kind}:${sanitizeMcpRefToken(name)}`,
group: framework,
name,
properties: baseProperties(filePath, framework, kind).concat(properties),
version: "configured",
};
}
function maybeAddFileSignals(properties, filePath, raw) {
const hiddenUnicodeScan = scanTextForHiddenUnicode(raw, {
syntax: syntaxForFile(filePath),
});
if (hiddenUnicodeScan.hasHiddenUnicode) {
addUniqueProperty(properties, "cdx:file:hasHiddenUnicode", "true");
addUniqueProperty(
properties,
"cdx:file:hiddenUnicodeCodePoints",
hiddenUnicodeScan.codePoints.join(","),
);
addUniqueProperty(
properties,
"cdx:file:hiddenUnicodeLineNumbers",
hiddenUnicodeScan.lineNumbers.join(","),
);
}
const credentialIndicators = credentialIndicatorsForText(raw);
if (credentialIndicators.length) {
addUniqueProperty(properties, "cdx:agent:credentialExposure", "true");
addUniqueProperty(
properties,
"cdx:agent:credentialRiskIndicators",
credentialIndicators.join(","),
);
}
}
function parseSkillFile(filePath, raw) {
const framework = frameworkForFile(filePath, raw);
const { metadata } = parseFrontmatter(raw);
if (!metadata?.name && !metadata?.description) {
return [];
}
const component = createComponent(
filePath,
framework,
"skill-file",
metadata?.name || basename(dirname(filePath)),
"file",
);
addUniqueProperty(component.properties, "cdx:skill:name", metadata?.name);
addUniqueProperty(
component.properties,
"cdx:skill:description",
metadata?.description,
);
addUniqueProperty(
component.properties,
"cdx:skill:license",
metadata?.license,
);
addUniqueProperty(
component.properties,
"cdx:skill:compatibility",
metadata?.compatibility,
);
if (metadata?.metadata && typeof metadata.metadata === "object") {
addUniqueProperty(
component.properties,
"cdx:skill:metadata",
metadata.metadata,
);
}
maybeAddFileSignals(component.properties, filePath, raw);
return [component];
}
function parseAgentMarkdown(filePath, raw) {
const framework = frameworkForFile(filePath, raw);
const { metadata } = parseFrontmatter(raw);
if (!metadata?.description && !metadata?.name) {
return [];
}
const component = createComponent(
filePath,
framework,
"agent-definition",
metadata?.name || basename(filePath, extname(filePath)),
);
addUniqueProperty(
component.properties,
"cdx:agent:description",
metadata?.description,
);
addUniqueProperty(component.properties, "cdx:agent:mode", metadata?.mode);
addUniqueProperty(component.properties, "cdx:agent:model", metadata?.model);
addUniqueProperty(
component.properties,
"cdx:agent:providerOverride",
metadata?.provider,
);
if (Array.isArray(metadata?.tools)) {
addUniqueProperty(
component.properties,
"cdx:agent:tools",
metadata.tools.join(","),
);
}
if (Array.isArray(metadata?.disallowedTools)) {
addUniqueProperty(
component.properties,
"cdx:agent:disallowedTools",
metadata.disallowedTools.join(","),
);
}
maybeAddFileSignals(component.properties, filePath, raw);
return [component];
}
function parseNanocoderCommand(filePath, raw) {
const { metadata } = parseFrontmatter(raw);
const component = createComponent(
filePath,
"nanocoder",
"custom-command",
basename(filePath, extname(filePath)),
);
addUniqueProperty(
component.properties,
"cdx:tool:description",
metadata?.description,
);
addUniqueProperty(
component.properties,
"cdx:tool:category",
metadata?.category,
);
addUniqueProperty(component.properties, "cdx:tool:author", metadata?.author);
addUniqueProperty(
component.properties,
"cdx:tool:version",
metadata?.version,
);
if (Array.isArray(metadata?.aliases)) {
addUniqueProperty(
component.properties,
"cdx:tool:aliases",
metadata.aliases.join(","),
);
}
if (Array.isArray(metadata?.tags)) {
addUniqueProperty(
component.properties,
"cdx:tool:tags",
metadata.tags.join(","),
);
}
if (Array.isArray(metadata?.triggers)) {
addUniqueProperty(
component.properties,
"cdx:tool:triggers",
metadata.triggers.join(","),
);
}
maybeAddFileSignals(component.properties, filePath, raw);
return [component];
}
function parseOpencodeTools(filePath, raw) {
if (!/\btool\s*\(/u.test(raw)) {
return [];
}
const components = [];
const baseName = basename(filePath, extname(filePath));
const exportMatches = [
...raw.matchAll(/export\s+const\s+([A-Za-z0-9_]+)\s*=\s*tool\s*\(/gu),
];
if (!exportMatches.length && /export\s+default\s+tool\s*\(/u.test(raw)) {
const component = createComponent(
filePath,
"opencode",
"custom-tool",
baseName,
);
const descriptionMatch = raw.match(/description:\s*["'`](.+?)["'`]/u);
addUniqueProperty(
component.properties,
"cdx:tool:description",
descriptionMatch?.[1],
);
maybeAddFileSignals(component.properties, filePath, raw);
components.push(component);
return components;
}
for (const [index, match] of exportMatches.entries()) {
const nextIndex =
index < exportMatches.length - 1
? exportMatches[index + 1].index
: raw.length;
const block = raw.slice(match.index, nextIndex);
const component = createComponent(
filePath,
"opencode",
"custom-tool",
`${baseName}_${match[1]}`,
);
const descriptionMatch = block.match(/description:\s*["'`](.+?)["'`]/u);
addUniqueProperty(
component.properties,
"cdx:tool:description",
descriptionMatch?.[1],
);
maybeAddFileSignals(component.properties, filePath, block);
components.push(component);
}
return components;
}
function parseOpencodeConfig(filePath, raw) {
let config;
try {
config = parseJsonLike(raw);
} catch {
return [];
}
const components = [];
for (const [agentName, agentConfig] of Object.entries(config?.agent || {})) {
const component = createComponent(
filePath,
"opencode",
"agent-config",
agentName,
);
addUniqueProperty(
component.properties,
"cdx:agent:description",
agentConfig?.description,
);
addUniqueProperty(
component.properties,
"cdx:agent:mode",
agentConfig?.mode,
);
addUniqueProperty(
component.properties,
"cdx:agent:model",
agentConfig?.model,
);
if (agentConfig?.permission) {
addUniqueProperty(
component.properties,
"cdx:agent:permission",
agentConfig.permission,
);
}
components.push(component);
}
return components;
}
function parseLanggraphConfig(filePath, raw) {
let config;
try {
config = parseJsonLike(raw);
} catch {
return { components: [], services: [] };
}
const components = [];
const services = [];
for (const [graphName, graphRef] of Object.entries(config?.graphs || {})) {
const component = createComponent(
filePath,
"langgraph",
"graph-definition",
graphName,
);
addUniqueProperty(
component.properties,
"cdx:agent:role",
"langgraph-graph",
);
addUniqueProperty(
component.properties,
"cdx:langgraph:graphEntryPoint",
graphRef,
);
addUniqueProperty(
component.properties,
"cdx:langgraph:envFile",
config?.env,
);
if (Array.isArray(config?.dependencies)) {
addUniqueProperty(
component.properties,
"cdx:langgraph:dependencies",
config.dependencies.join(","),
);
}
maybeAddFileSignals(component.properties, filePath, raw);
components.push(component);
services.push(
createService(filePath, "langgraph", "graph-service", graphName, [
{ name: "cdx:agent:inventorySource", value: "community-config" },
{ name: "cdx:agent:framework", value: "langgraph" },
{ name: "cdx:agent:role", value: "langgraph-graph" },
{ name: "cdx:langgraph:graphEntryPoint", value: String(graphRef) },
]),
);
}
return { components, services };
}
function parseCrewAiYaml(filePath, raw) {
let config;
try {
config = loadYaml(raw);
} catch {
return [];
}
if (!config || typeof config !== "object" || Array.isArray(config)) {
return [];
}
const kind = filePath.toLowerCase().includes("tasks.yaml")
? "crew-task"
: filePath.toLowerCase().includes("tools.yaml")
? "crew-tool"
: "crew-agent";
const components = [];
for (const [entryName, entryConfig] of Object.entries(config)) {
if (!entryConfig || typeof entryConfig !== "object") {
continue;
}
const component = createComponent(filePath, "crewai", kind, entryName);
addUniqueProperty(component.properties, "cdx:agent:role", kind);
addUniqueProperty(
component.properties,
"cdx:crewai:role",
entryConfig?.role,
);
addUniqueProperty(
component.properties,
"cdx:crewai:goal",
entryConfig?.goal,
);
addUniqueProperty(
component.properties,
"cdx:crewai:backstory",
entryConfig?.backstory,
);
addUniqueProperty(
component.properties,
"cdx:crewai:expectedOutput",
entryConfig?.expected_output,
);
if (Array.isArray(entryConfig?.tools)) {
addUniqueProperty(
component.properties,
"cdx:crewai:tools",
entryConfig.tools.join(","),
);
}
if (entryConfig?.agent) {
addUniqueProperty(
component.properties,
"cdx:crewai:assignedAgent",
entryConfig.agent,
);
}
maybeAddFileSignals(component.properties, filePath, raw);
components.push(component);
}
return components;
}
function captureQuotedValue(block, key) {
const patterns = [
new RegExp(`${key}\\s*=\\s*["'\`]([^"'\`]+)["'\`]`, "u"),
new RegExp(`${key}\\s*=\\s*dedent\\([^\\n]*?["'\`]([\\s\\S]*?)["'\`]`, "u"),
];
for (const pattern of patterns) {
const match = block.match(pattern);
if (match?.[1]) {
return match[1].replace(/\s+/gu, " ").trim();
}
}
return undefined;
}
function parseCrewAiPython(filePath, raw) {
if (!/\bfrom\s+crewai\s+import\s+(Agent|Task)\b/u.test(raw)) {
return [];
}
const isTaskFile = /\bfrom\s+crewai\s+import\s+Task\b/u.test(raw);
const constructorName = isTaskFile ? "Task" : "Agent";
const matches = [
...raw.matchAll(
new RegExp(
`def\\s+([A-Za-z0-9_]+)\\s*\\([^)]*\\):[\\s\\S]*?return\\s+${constructorName}\\(([\\s\\S]*?)\\n\\s*\\)`,
"gu",
),
),
];
const components = [];
for (const match of matches) {
const component = createComponent(
filePath,
"crewai",
isTaskFile ? "crew-task" : "crew-agent",
match[1],
);
addUniqueProperty(
component.properties,
"cdx:agent:role",
isTaskFile ? "crew-task" : "crew-agent",
);
addUniqueProperty(
component.properties,
"cdx:crewai:role",
captureQuotedValue(match[2], "role"),
);
addUniqueProperty(
component.properties,
"cdx:crewai:goal",
captureQuotedValue(match[2], "goal"),
);
addUniqueProperty(
component.properties,
"cdx:crewai:backstory",
captureQuotedValue(match[2], "backstory"),
);
addUniqueProperty(
component.properties,
"cdx:crewai:description",
captureQuotedValue(match[2], "description"),
);
addUniqueProperty(
component.properties,
"cdx:crewai:expectedOutput",
captureQuotedValue(match[2], "expected_output"),
);
const toolsMatch = match[2].match(/tools\s*=\s*\[([^\]]+)\]/u);
if (toolsMatch?.[1]) {
addUniqueProperty(
component.properties,
"cdx:crewai:tools",
toolsMatch[1].replace(/\s+/gu, ""),
);
}
maybeAddFileSignals(component.properties, filePath, match[2]);
components.push(component);
}
return components;
}
export const communityAiConfigParser = {
id: "community-ai-config",
patterns: COMMUNITY_AI_PATTERNS,
parse(files, _options = {}) {
const components = [];
const services = [];
for (const filePath of [...new Set(files || [])]) {
let raw;
try {
raw = readFileSync(filePath, "utf-8");
} catch {
continue;
}
const lowerPath = normalizeFilePath(filePath).toLowerCase();
if (
lowerPath.endsWith("opencode.json") ||
lowerPath.endsWith("opencode.jsonc")
) {
components.push(...parseOpencodeConfig(filePath, raw));
continue;
}
if (lowerPath.endsWith("langgraph.json")) {
const parsed = parseLanggraphConfig(filePath, raw);
components.push(...parsed.components);
services.push(...parsed.services);
continue;
}
if (
lowerPath.includes("/.opencode/agents/") ||
lowerPath.includes("/.nanocoder/agents/")
) {
components.push(...parseAgentMarkdown(filePath, raw));
continue;
}
if (
lowerPath.includes("/.opencode/skills/") ||
lowerPath.includes("/.claude/skills/") ||
lowerPath.includes("/.agents/skills/") ||
lowerPath.includes("/.nanocoder/skills/")
) {
components.push(...parseSkillFile(filePath, raw));
continue;
}
if (
lowerPath.includes("/.opencode/tools/") ||
lowerPath.includes("/.opencode/tool/")
) {
components.push(...parseOpencodeTools(filePath, raw));
continue;
}
if (lowerPath.includes("/.nanocoder/commands/")) {
components.push(...parseNanocoderCommand(filePath, raw));
continue;
}
if (lowerPath.endsWith("agents.py") || lowerPath.endsWith("tasks.py")) {
components.push(...parseCrewAiPython(filePath, raw));
continue;
}
if (
lowerPath.includes("/config/agents.yaml") ||
lowerPath.includes("/config/tasks.yaml") ||
lowerPath.includes("/config/tools.yaml")
) {
components.push(...parseCrewAiYaml(filePath, raw));
}
}
return { components, services };
},
};