UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

446 lines (428 loc) 12.9 kB
import process from "node:process"; const ASTGEN_DEFAULT_IGNORE_DIRS = [ "venv", "docs", "e2e", "e2e-beta", "examples", "cypress", "jest-cache", "eslint-rules", "codemods", "flow-typed", "i18n", ]; const ATOM_JS_LANGUAGES = new Set([ "javascript", "js", "jsx", "node", "nodejs", "typescript", "ts", "tsx", ]); function escapeScalaRegexLiteral(value) { return value.replace(/[\\^$*+?.()|[\]{}]/g, "\\$&"); } function normalizeGlobPattern(pattern) { pattern = `${pattern}`; let normalizedPattern = ""; for (let i = 0; i < pattern.length; i++) { const char = pattern[i]; if (char !== "\\") { normalizedPattern += char; continue; } const nextChar = pattern[i + 1]; if (nextChar && "*?[]{}()!+@,".includes(nextChar)) { normalizedPattern += char; normalizedPattern += nextChar; i++; } else { normalizedPattern += "/"; } } return normalizedPattern.replace(/^\.\//, ""); } function splitGlobAlternates(value, separator = ",") { const alternates = []; let current = ""; let braceDepth = 0; let bracketDepth = 0; let parenDepth = 0; for (let i = 0; i < value.length; i++) { const char = value[i]; if (char === "\\") { current += char; if (i + 1 < value.length) { current += value[++i]; } continue; } if (char === "[" && bracketDepth === 0) { bracketDepth++; } else if (char === "]" && bracketDepth > 0) { bracketDepth--; } else if (!bracketDepth) { if (char === "{") { braceDepth++; } else if (char === "}" && braceDepth > 0) { braceDepth--; } else if (char === "(") { parenDepth++; } else if (char === ")" && parenDepth > 0) { parenDepth--; } else if (char === separator && braceDepth === 0 && parenDepth === 0) { alternates.push(current); current = ""; continue; } } current += char; } alternates.push(current); return alternates; } function findClosingGlobToken(value, startIndex, openChar, closeChar) { if (openChar === "[") { for (let i = startIndex + 1; i < value.length; i++) { if (value[i] === "\\") { i++; } else if (value[i] === closeChar) { return i; } } return -1; } let depth = 0; let inBracket = false; for (let i = startIndex; i < value.length; i++) { const char = value[i]; if (char === "\\") { i++; continue; } if (char === "[" && !inBracket) { inBracket = true; } else if (char === "]" && inBracket) { inBracket = false; } else if (!inBracket) { if (char === openChar) { depth++; } else if (char === closeChar) { depth--; if (depth === 0) { return i; } } } } return -1; } function globCharClassToRegex(value) { if (!value.length) { return "\\["; } let classValue = value; let prefix = ""; if (classValue[0] === "!" || classValue[0] === "^") { prefix = "^"; classValue = classValue.slice(1); } if (!classValue.length) { return "\\["; } classValue = classValue.replace(/\\/g, "\\\\").replace(/]/g, "\\]"); return `[${prefix}${classValue}]`; } function globSegmentToScalaRegex(segment) { let regex = ""; for (let i = 0; i < segment.length; i++) { const char = segment[i]; const nextChar = segment[i + 1]; if (char === "\\") { if (i + 1 < segment.length) { regex += escapeScalaRegexLiteral(segment[++i]); } else { regex += "\\\\"; } } else if (char === "*" && nextChar !== "(") { regex += "[^/\\\\]*"; } else if (char === "?" && nextChar !== "(") { regex += "[^/\\\\]"; } else if (char === "[") { const bracketEnd = findClosingGlobToken(segment, i, "[", "]"); if (bracketEnd === -1) { regex += "\\["; } else { regex += globCharClassToRegex(segment.slice(i + 1, bracketEnd)); i = bracketEnd; } } else if (char === "{") { const braceEnd = findClosingGlobToken(segment, i, "{", "}"); if (braceEnd === -1) { regex += "\\{"; } else { const alternates = splitGlobAlternates( segment.slice(i + 1, braceEnd), ).map((alternate) => globSegmentToScalaRegex(alternate)); regex += `(?:${alternates.join("|")})`; i = braceEnd; } } else if (["@", "?", "+", "*", "!"].includes(char) && nextChar === "(") { const parenEnd = findClosingGlobToken(segment, i + 1, "(", ")"); if (parenEnd === -1) { regex += escapeScalaRegexLiteral(char); } else { const alternates = splitGlobAlternates( segment.slice(i + 2, parenEnd), "|", ).map((alternate) => globSegmentToScalaRegex(alternate)); const alternateRegex = `(?:${alternates.join("|")})`; if (char === "@") { regex += alternateRegex; } else if (char === "?") { regex += `${alternateRegex}?`; } else if (char === "+") { regex += `${alternateRegex}+`; } else if (char === "*") { regex += `${alternateRegex}*`; } else { regex += `(?!(?:${alternates.join("|")})$)[^/\\\\]*`; } i = parenEnd; } } else { regex += escapeScalaRegexLiteral(char); } } return regex; } function getExcludePatterns(options = {}) { if (!Array.isArray(options.exclude)) { return []; } return options.exclude .flatMap((pattern) => { pattern = `${pattern}`; return pattern.includes(",") && !pattern.includes("{") ? pattern.split(",") : [pattern]; }) .map((pattern) => pattern.trim()) .filter(Boolean) .filter((pattern) => !pattern.startsWith("!")); } function extractIgnoreDirsFromExcludePatterns( patterns, includeExactPathFragments = false, ) { const ignoreDirs = new Set(); for (const pattern of patterns) { const normalizedPattern = normalizeGlobPattern(pattern); const isExactPath = !/[!*?{}[\]]/.test(normalizedPattern); const segments = normalizedPattern.split("/").filter(Boolean); const literalSegments = segments.filter( (segment) => !/[!*?{}[\]]/.test(segment) && segment !== "." && segment !== "..", ); if (!literalSegments.length) { continue; } const dirName = literalSegments.at(-1); if ( dirName && ((includeExactPathFragments && isExactPath) || !dirName.includes(".") || segments.at(-1) !== dirName) ) { ignoreDirs.add(dirName); } } return Array.from(ignoreDirs); } function globToScalaRegexFragment(pattern) { pattern = normalizeGlobPattern(pattern); const isAbsolute = pattern.startsWith("/"); const segments = pattern.split("/").filter(Boolean); if (!segments.length) { return "$^"; } if (segments.length === 1 && segments[0] === "**") { return ".*"; } let regex = isAbsolute ? "^[/\\\\]" : "(?:^|.*[/\\\\])"; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const isLast = i === segments.length - 1; const nextSegment = segments[i + 1]; if (segment === "**") { if (i === 0) { continue; } if (isLast) { regex += "(?:[/\\\\].*)?"; } else { regex += "(?:[/\\\\][^/\\\\]+)*[/\\\\]"; } continue; } regex += globSegmentToScalaRegex(segment); if (!isLast && nextSegment !== "**") { regex += "[/\\\\]"; } } return `${regex}$`; } /** * Convert cdxgen's glob-style exclude patterns to a Scala/Java regex string. * * @param {string[]} patterns Glob patterns from cdxgen's `--exclude` option * @returns {string|undefined} Scala-compatible regex or undefined when empty */ export function globPatternsToAtomIgnoreRegex(patterns = []) { const fragments = getExcludePatterns({ exclude: patterns }).map((pattern) => globToScalaRegexFragment(pattern), ); if (!fragments.length) { return undefined; } return `(?:${fragments.join("|")})`; } export function isPathExcludedByGlobPatterns(filePath, patterns = []) { const atomIgnoreRegex = globPatternsToAtomIgnoreRegex(patterns); if (!atomIgnoreRegex) { return false; } const normalizedPath = `${filePath}`.replace(/\\/g, "/").replace(/^\.\//, ""); const regex = new RegExp(atomIgnoreRegex); return regex.test(normalizedPath) || regex.test(`./${normalizedPath}`); } export function filterAtomSlicesByExcludePatterns(sliceData, patterns = []) { if (!sliceData || !getExcludePatterns({ exclude: patterns }).length) { return sliceData; } const shouldKeepFile = (fileName) => !fileName || !isPathExcludedByGlobPatterns(fileName, patterns); if (Array.isArray(sliceData)) { return sliceData.filter((slice) => shouldKeepFile(slice.fileName)); } const filteredSliceData = { ...sliceData }; if (Array.isArray(filteredSliceData.objectSlices)) { filteredSliceData.objectSlices = filteredSliceData.objectSlices.filter( (slice) => shouldKeepFile(slice.fileName), ); } if (Array.isArray(filteredSliceData.userDefinedTypes)) { filteredSliceData.userDefinedTypes = filteredSliceData.userDefinedTypes.filter((slice) => shouldKeepFile(slice.fileName), ); } if (Array.isArray(filteredSliceData.reachables)) { filteredSliceData.reachables = filteredSliceData.reachables.filter( (reachable) => (reachable.flows || []).every((flow) => shouldKeepFile(flow.parentFileName || flow.fileName), ), ); } if ( filteredSliceData.graph?.nodes && Array.isArray(filteredSliceData.paths) ) { const excludedNodeIds = new Set( filteredSliceData.graph.nodes .filter((node) => !shouldKeepFile(node.parentFileName || node.fileName)) .map((node) => node.id), ); filteredSliceData.paths = filteredSliceData.paths.filter((path) => path.every((nodeId) => !excludedNodeIds.has(nodeId)), ); const retainedNodeIds = new Set(filteredSliceData.paths.flat()); filteredSliceData.graph = { ...filteredSliceData.graph, nodes: filteredSliceData.graph.nodes.filter( (node) => retainedNodeIds.has(node.id) || !excludedNodeIds.has(node.id), ), edges: (filteredSliceData.graph.edges || []).filter((edge) => { const source = edge.src ?? edge.source; const destination = edge.dst ?? edge.destination; return ( !excludedNodeIds.has(source) && !excludedNodeIds.has(destination) ); }), }; } return filteredSliceData; } function mergeCsvValues(...valueLists) { const values = new Set(); for (const valueList of valueLists) { if (Array.isArray(valueList)) { valueList.forEach((value) => { values.add(`${value}`.trim()); }); } else if (typeof valueList === "string" && valueList.length) { valueList.split(",").forEach((value) => { values.add(value.trim()); }); } } return Array.from(values).filter(Boolean).join(","); } function mergeRegexValues(...regexValues) { const values = regexValues .map((regexValue) => `${regexValue || ""}`.trim()) .filter(Boolean); if (!values.length) { return undefined; } return values.map((regexValue) => `(?:${regexValue})`).join("|"); } /** * Build additional environment variables for Atom from cdxgen CLI options. * * @param {Object} options CLI options * @param {string} language Atom language name * @returns {Object} Environment variables to pass to Atom */ export function buildAtomCommandEnv(options = {}, language = "") { const excludePatterns = getExcludePatterns(options); if (!excludePatterns.length) { return {}; } const chenIgnoreDirs = mergeCsvValues( process.env.CHEN_IGNORE_DIRS, extractIgnoreDirsFromExcludePatterns(excludePatterns, true), ); const env = {}; if (chenIgnoreDirs) { env.CHEN_IGNORE_DIRS = chenIgnoreDirs; } const atomIgnoreRegex = globPatternsToAtomIgnoreRegex(excludePatterns); const normalizedLanguage = `${language}`.toLowerCase(); if (ATOM_JS_LANGUAGES.has(normalizedLanguage)) { const astgenBaseIgnoreDirs = process.env.ASTGEN_IGNORE_DIRS === undefined ? ASTGEN_DEFAULT_IGNORE_DIRS : process.env.ASTGEN_IGNORE_DIRS; const astgenIgnoreDirs = mergeCsvValues( astgenBaseIgnoreDirs, "node_modules", extractIgnoreDirsFromExcludePatterns(excludePatterns), ); if (astgenIgnoreDirs) { env.ASTGEN_IGNORE_DIRS = astgenIgnoreDirs; } const astgenIgnoreFilePattern = mergeRegexValues( process.env.ASTGEN_IGNORE_FILE_PATTERN, atomIgnoreRegex, ); if (astgenIgnoreFilePattern) { env.ASTGEN_IGNORE_FILE_PATTERN = astgenIgnoreFilePattern; } } return env; }