UNPKG

@cyclonedx/cdxgen

Version:

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

1,105 lines (1,068 loc) 31.9 kB
import { Buffer } from "node:buffer"; import { spawnSync } from "node:child_process"; import { arch, homedir } from "node:os"; import { delimiter, dirname, join } from "node:path"; import process from "node:process"; import { compareLoose } from "semver"; import { CARGO_CMD, DEBUG_MODE, DOTNET_CMD, GCC_CMD, GO_CMD, MAX_BUFFER, NODE_CMD, NPM_CMD, RUBY_CMD, RUSTC_CMD, SWIFT_CMD, TIMEOUT_MS, getJavaCommand, getPythonCommand, getTmpDir, isMac, isWin, safeExistsSync, } from "./utils.js"; export const GIT_COMMAND = process.env.GIT_CMD || "git"; // sdkman tool aliases export const SDKMAN_JAVA_TOOL_ALIASES = { java8: process.env.JAVA8_TOOL || "8.0.452-amzn", // Temurin no longer offers java8 :( java11: process.env.JAVA11_TOOL || "11.0.27-tem", java17: process.env.JAVA17_TOOL || "17.0.15-tem", java21: process.env.JAVA21_TOOL || "21.0.7-tem", java22: process.env.JAVA22_TOOL || "22.0.2-tem", java23: process.env.JAVA23_TOOL || "23.0.2-tem", java24: process.env.JAVA24_TOOL || "24-tem", }; /** * Retrieves a git config item * @param {string} configKey Git config key * @param {string} dir repo directory * * @returns Output from git config or undefined */ export function getGitConfig(configKey, dir) { return execGitCommand(dir, ["config", "--get", configKey]); } /** * Retrieves the git origin url * @param {string} dir repo directory * * @returns Output from git config or undefined */ export function getOriginUrl(dir) { return getGitConfig("remote.origin.url", dir); } /** * Retrieves the git branch name * @param {string} configKey Git config key * @param {string} dir repo directory * * @returns Output from git config or undefined */ export function getBranch(_configKey, dir) { return execGitCommand(dir, ["rev-parse", "--abbrev-ref", "HEAD"]); } /** * Retrieves the tree and parent hash for a git repo * @param {string} dir repo directory * * @returns Output from git cat-file or undefined */ export function gitTreeHashes(dir) { const treeHashes = {}; const output = execGitCommand(dir, ["cat-file", "commit", "HEAD"]); if (output) { output.split("\n").forEach((l) => { l = l.replace("\r", ""); if (l === "\n" || l.startsWith("#")) { return; } if (l.startsWith("tree") || l.startsWith("parent")) { const tmpA = l.split(" "); if (tmpA && tmpA.length === 2) { treeHashes[tmpA[0]] = tmpA[1]; } } }); } return treeHashes; } /** * Retrieves the files list from git * @param {string} dir repo directory * * @returns Output from git config or undefined */ export function listFiles(dir) { const filesList = []; const output = execGitCommand(dir, [ "ls-tree", "-l", "-r", "--full-tree", "HEAD", ]); if (output) { output.split("\n").forEach((l) => { l = l.replace("\r", ""); if (l === "\n" || l.startsWith("#")) { return; } const tmpA = l.split(" "); if (tmpA && tmpA.length >= 5) { const lastParts = tmpA[tmpA.length - 1].split("\t"); filesList.push({ hash: tmpA[2], name: lastParts[lastParts.length - 1], omniborId: `gitoid:blob:sha1:${tmpA[2]}`, swhid: `swh:1:rev:${tmpA[2]}`, }); } }); } return filesList; } /** * Execute a git command * * @param {string} dir Repo directory * @param {Array} args arguments to git command * * @returns Output from the git command */ export function execGitCommand(dir, args) { return getCommandOutput(GIT_COMMAND, dir, args); } /** * Collect Java version and installed modules * * @param {string} dir Working directory * @returns Object containing the java details */ export function collectJavaInfo(dir) { const versionDesc = getCommandOutput(getJavaCommand(), dir, ["--version"]); const moduleDesc = getCommandOutput(getJavaCommand(), dir, ["--list-modules"]) || ""; if (versionDesc) { return { type: "platform", name: "java", version: versionDesc.split("\n")[0].replace("java ", ""), description: versionDesc, properties: [ { name: "java:modules", value: moduleDesc.replaceAll("\n", ", "), }, ], }; } return undefined; } /** * Collect dotnet version * * @param {string} dir Working directory * @returns Object containing dotnet details */ export function collectDotnetInfo(dir) { const versionDesc = getCommandOutput(DOTNET_CMD, dir, ["--version"]); const moduleDesc = getCommandOutput(DOTNET_CMD, dir, ["--list-runtimes"]) || ""; if (versionDesc) { return { type: "platform", name: "dotnet", version: versionDesc.trim(), description: moduleDesc.replaceAll("\n", "\\n"), }; } return undefined; } /** * Collect python version * * @param {string} dir Working directory * @returns Object containing python details */ export function collectPythonInfo(dir) { const versionDesc = getCommandOutput(getPythonCommand(), dir, ["--version"]); const moduleDesc = getCommandOutput(getPythonCommand(), dir, ["-m", "pip", "--version"]) || ""; if (versionDesc) { return { type: "platform", name: "python", version: versionDesc.replace("Python ", ""), description: moduleDesc.replaceAll("\n", "\\n"), }; } return undefined; } /** * Collect node version * * @param {string} dir Working directory * @returns Object containing node details */ export function collectNodeInfo(dir) { const versionDesc = getCommandOutput(NODE_CMD, dir, ["--version"]); let moduleDesc = getCommandOutput(NPM_CMD, dir, ["--version"]); if (moduleDesc) { moduleDesc = `npm: ${moduleDesc}`; } if (versionDesc) { return { type: "platform", name: "node", version: versionDesc.trim(), description: moduleDesc, }; } return undefined; } /** * Collect gcc version * * @param {string} dir Working directory * @returns Object containing gcc details */ export function collectGccInfo(dir) { const versionDesc = getCommandOutput(GCC_CMD, dir, ["--version"]); const moduleDesc = getCommandOutput(GCC_CMD, dir, ["-print-search-dirs"]); if (versionDesc) { return { type: "platform", name: "gcc", version: versionDesc.split("\n")[0], description: moduleDesc.replaceAll("\n", "\\n"), }; } return undefined; } /** * Collect rust version * * @param {string} dir Working directory * @returns Object containing rust details */ export function collectRustInfo(dir) { const versionDesc = getCommandOutput(RUSTC_CMD, dir, ["--version"]); const moduleDesc = getCommandOutput(CARGO_CMD, dir, ["--version"]); if (versionDesc) { return { type: "platform", name: "rustc", version: versionDesc.trim(), description: moduleDesc.trim(), }; } return undefined; } /** * Collect go version * * @param {string} dir Working directory * @returns Object containing go details */ export function collectGoInfo(dir) { const versionDesc = getCommandOutput(GO_CMD, dir, ["version"]); if (versionDesc) { return { type: "platform", name: "go", version: versionDesc.trim(), }; } return undefined; } /** * Collect swift version * * @param {string} dir Working directory * @returns Object containing swift details */ export function collectSwiftInfo(dir) { const versionDesc = getCommandOutput(SWIFT_CMD, dir, ["--version"]); if (versionDesc) { return { type: "platform", name: "swift", version: versionDesc.trim(), }; } return undefined; } /** * Collect Ruby version * * @param {string} dir Working directory * @returns Object containing Ruby details */ export function collectRubyInfo(dir) { const versionDesc = getCommandOutput(RUBY_CMD, dir, ["--version"]); if (versionDesc) { return { type: "platform", name: "ruby", version: versionDesc.trim(), }; } return undefined; } /** * Method to run a swift command * * @param {String} dir Working directory * @param {Array} args Command arguments * @returns Object containing swift details */ export function runSwiftCommand(dir, args) { return getCommandOutput(SWIFT_CMD, dir, args); } export function collectEnvInfo(dir) { const infoComponents = []; let cmp = collectJavaInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectDotnetInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectPythonInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectNodeInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectGccInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectRustInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectGoInfo(dir); if (cmp) { infoComponents.push(cmp); } cmp = collectRubyInfo(dir); if (cmp) { infoComponents.push(cmp); } return infoComponents; } /** * Execute any command to retrieve the output * * @param {*} cmd Command to execute * @param {*} dir working directory * @param {*} args arguments * @returns String output from the command or undefined in case of error */ const getCommandOutput = (cmd, dir, args) => { let commandToUse = cmd; // If the command includes space, automatically move it to the front of the args. if (cmd?.trim().includes(" ")) { const tmpA = cmd.split(" "); commandToUse = tmpA.shift(); if (args?.length && tmpA.length) { args = tmpA.concat(args); } } if (DEBUG_MODE) { console.log(`Executing ${commandToUse} ${args.join(" ")} in ${dir}`); } const result = spawnSync(commandToUse, args, { cwd: dir, encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, maxBuffer: MAX_BUFFER, }); if (result.status !== 0 || result.error) { return undefined; } const stdout = result.stdout; if (stdout) { const cmdOutput = Buffer.from(stdout).toString(); return cmdOutput.trim().replaceAll("\r", ""); } }; /** * Method to check if sdkman is available. */ export function isSdkmanAvailable() { let isSdkmanSetup = ["SDKMAN_DIR", "SDKMAN_CANDIDATES_DIR"].filter( (v) => process.env[v] && safeExistsSync(process.env[v]), ).length >= 1; if (!isSdkmanSetup && safeExistsSync(join(homedir(), ".sdkman", "bin"))) { process.env.SDKMAN_DIR = join(homedir(), ".sdkman"); process.env.SDKMAN_CANDIDATES_DIR = join( homedir(), ".sdkman", "candidates", ); isSdkmanSetup = true; } return isSdkmanSetup; } /** * Method to check if nvm is available. */ export function isNvmAvailable() { const result = spawnSync( process.env.SHELL || "bash", ["-i", "-c", process.env.NVM_CMD || "nvm"], { encoding: "utf-8", shell: process.env.SHELL || true, }, ); return result.status === 0; } /** * Method to check if a given sdkman tool is installed and available. * * @param {String} toolType Tool type such as java, gradle, maven etc. * @param {String} toolName Tool name with version. Eg: 22.0.2-tem * * @returns {Boolean} true if the tool is available. false otherwise. */ export function isSdkmanToolAvailable(toolType, toolName) { toolName = getSdkmanToolFullname(toolName); let isToolAvailable = process.env.SDKMAN_CANDIDATES_DIR && safeExistsSync( join(process.env.SDKMAN_CANDIDATES_DIR, toolType, toolName, "bin"), ); if ( !isToolAvailable && safeExistsSync( join(homedir(), ".sdkman", "candidates", toolType, toolName, "bin"), ) ) { process.env.SDKMAN_CANDIDATES_DIR = join( homedir(), ".sdkman", "candidates", ); isToolAvailable = true; } return isToolAvailable; } /** * Method to install and use a given sdkman tool. * * @param {String} toolType Tool type such as java, gradle, maven etc. * @param {String} toolName Tool name with version. Eg: 22.0.2-tem * * @returns {Boolean} true if the tool is available. false otherwise. */ export function installSdkmanTool(toolType, toolName) { if (isWin) { return false; } toolName = getSdkmanToolFullname(toolName); let result = undefined; if (!isSdkmanToolAvailable(toolType, toolName)) { let installDir = ""; if (process.env.SDKMAN_CANDIDATES_DIR) { installDir = join(process.env.SDKMAN_CANDIDATES_DIR, toolType); } console.log("About to install", toolType, toolName, installDir); result = spawnSync( process.env.SHELL || "bash", [ "-i", "-c", `"echo -e "no" | sdk install ${toolType} ${toolName} ${installDir}"`.trim(), ], { encoding: "utf-8", shell: process.env.SHELL || true, timeout: TIMEOUT_MS, }, ); if (DEBUG_MODE) { if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } } if (result.status === 1 || result.error) { console.log( "Unable to install", toolType, toolName, "due to below errors.", ); return false; } } const toolUpper = toolType.toUpperCase(); // Set process env variables if ( process.env[`${toolUpper}_HOME`] && process.env[`${toolUpper}_HOME`].includes("current") ) { process.env[`${toolUpper}_HOME`] = process.env[`${toolUpper}_HOME`].replace( "current", toolName, ); console.log( `${toolUpper}_HOME`, "set to", process.env[`${toolUpper}_HOME`], ); } else if ( process.env.SDKMAN_CANDIDATES_DIR && safeExistsSync(join(process.env.SDKMAN_CANDIDATES_DIR, toolType, toolName)) ) { process.env[`${toolUpper}_HOME`] = join( process.env.SDKMAN_CANDIDATES_DIR, toolType, toolName, ); console.log( `${toolUpper}_HOME`, "set to", process.env[`${toolUpper}_HOME`], ); } else { console.log( "Directory", join(process.env.SDKMAN_CANDIDATES_DIR, toolType, toolName), "is not found", ); } const toolCurrentBin = join(toolType, "current", "bin"); if (process.env?.PATH.includes(toolCurrentBin)) { process.env.PATH = process.env.PATH.replace( toolCurrentBin, join(toolType, toolName, "bin"), ); } else if (process.env.SDKMAN_CANDIDATES_DIR) { const fullToolBinDir = join( process.env.SDKMAN_CANDIDATES_DIR, toolType, toolName, "bin", ); if (!process.env?.PATH?.includes(fullToolBinDir)) { process.env.PATH = `${fullToolBinDir}${delimiter}${process.env.PATH}`; } } return true; } /** * Method to check if a given nvm tool is installed and available. * * @param {String} toolName Tool name with version. Eg: 22.0.2-tem * * @returns {String} path of nvm if present, otherwise false */ export function getNvmToolDirectory(toolName) { const resultWhichNode = spawnSync( process.env.SHELL || "bash", ["-i", "-c", `"nvm which ${toolName}"`], { encoding: "utf-8", shell: process.env.SHELL || true, timeout: TIMEOUT_MS, }, ); if (DEBUG_MODE) { if (resultWhichNode.stdout) { console.log(resultWhichNode.stdout); } if (resultWhichNode.stderr) { console.log(resultWhichNode.stderr); } } if (resultWhichNode.status !== 0 || resultWhichNode.stderr) { return; } return dirname(resultWhichNode.stdout.trim()); } /** * Method to return nvm tool path * * @param {String} toolVersion Tool name with version. Eg: 22.0.2-tem * * @returns {String} path of the tool if not found installs and then returns paths. false if encounters an error. */ export function getOrInstallNvmTool(toolVersion) { const nvmNodePath = getNvmToolDirectory(toolVersion); if (!nvmNodePath) { // nvm couldn't directly use toolName so maybe needs to be installed const resultInstall = spawnSync( process.env.SHELL || "bash", ["-i", "-c", `"nvm install ${toolVersion}"`], { encoding: "utf-8", shell: process.env.SHELL || true, timeout: TIMEOUT_MS, }, ); if (DEBUG_MODE) { if (resultInstall.stdout) { console.log(resultInstall.stdout); } if (resultInstall.stderr) { console.log(resultInstall.stderr); } } if (resultInstall.status !== 0) { // There was some problem install the tool // output has already been printed out return false; } const nvmNodePath = getNvmToolDirectory(toolVersion); if (nvmNodePath) { return nvmNodePath; } return false; } return nvmNodePath; } /** * Retrieve sdkman tool full name */ function getSdkmanToolFullname(toolName) { return SDKMAN_JAVA_TOOL_ALIASES[toolName] || toolName; } /** * Method to check if rbenv is available. * * @returns {Boolean} true if rbenv is available. false otherwise. */ export function isRbenvAvailable() { let result = spawnSync( process.env.SHELL || "bash", ["-i", "-c", process.env.RBENV_CMD || "rbenv", "--version"], { encoding: "utf-8", shell: process.env.SHELL || true, timeout: TIMEOUT_MS, }, ); if (result.status !== 0) { result = spawnSync(process.env.RBENV_CMD || "rbenv", ["--version"], { shell: isWin, encoding: "utf-8", }); return result.status === 0; } } export function rubyVersionDir(rubyVersion) { return process.env.RBENV_ROOT ? join(process.env.RBENV_ROOT, "versions", rubyVersion, "bin") : join(homedir(), ".rbenv", "versions", rubyVersion, "bin"); } /** * Perform bundle install using Ruby container images. Not working cleanly yet. * * @param rubyVersion Ruby version * @param cdxgenGemHome Gem Home * @param filePath Path */ export function bundleInstallWithDocker(rubyVersion, cdxgenGemHome, filePath) { const ociCmd = process.env.DOCKER_CMD || "docker"; const ociArgs = [ "run", "--rm", "-e", "GEM_HOME=/gems", "-v", `/tmp:${getTmpDir()}:rw`, "-v", `${filePath}:/app:rw`, "-v", `${cdxgenGemHome}:/gems:rw`, "-w", "/app", "-it", `docker.io/ruby:${rubyVersion}`, "bash", "-c", "bundle", "install", ]; console.log(`Performing bundle install with: ${ociCmd} ${ociArgs.join(" ")}`); const result = spawnSync(ociCmd, ociArgs, { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, stdio: "inherit", }); if (result.error || result.status !== 0) { return false; } if (safeExistsSync(join(filePath, "Gemfile.lock"))) { console.log( "Gemfile.lock was generated successfully. Thank you for trying this feature!", ); } return result.status === 0; } /** * Install a particular ruby version using rbenv. * * @param rubyVersion Ruby version to install * @param filePath File path */ export function installRubyVersion(rubyVersion, filePath) { if (!rubyVersion) { return { fullToolBinDir: undefined, status: false }; } const existingRuby = collectRubyInfo(filePath); if (existingRuby?.version?.startsWith(`ruby ${rubyVersion} `)) { return { fullToolBinDir: undefined, status: true }; } const fullToolBinDir = rubyVersionDir(rubyVersion); if (safeExistsSync(fullToolBinDir)) { const result = spawnSync( process.env.RBENV_CMD || "rbenv", ["local", rubyVersion], { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, }, ); if (result.error || result.status !== 0) { if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } } if (result.status === 0) { return { fullToolBinDir, status: true }; } } // Check if we're trying to install Ruby 1.x or 2.x if (rubyVersion.startsWith("1.")) { console.log( `Ruby version ${rubyVersion} requires very old versions of Linux such as debian:8. Consider using the container image "ghcr.io/cyclonedx/debian-ruby18:master" to build the application first and then invoke cdxgen with the arguments "--lifecycle pre-build".`, ); console.log("The below install step is likely to fail."); } else if ( rubyVersion.startsWith("2.") && process.env?.CDXGEN_IN_CONTAINER !== "true" ) { console.log( `Installing Ruby version ${rubyVersion} requires specific development libraries. Consider using the custom container image "ghcr.io/cyclonedx/cdxgen-debian-ruby26:v11" instead.`, ); console.log("The below install step is likely to fail."); } console.log( `Attempting to install Ruby ${rubyVersion} using rbenv. This might take a while ...`, ); if (process.env?.CDXGEN_IN_CONTAINER === "true") { console.log( `To speed up this step, use bind mounts. Example: "--mount type=bind,src=/tmp/rbenv,dst=/root/.rbenv/versions/${rubyVersion}"`, ); } const result = spawnSync( process.env.RBENV_CMD || "rbenv", ["install", rubyVersion], { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, }, ); if (result.error || result.status !== 0) { if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } if (isMac) { console.log( "Try running the commands `sudo xcode-select --install` followed by `xcodebuild -runFirstLaunch`.", ); console.log( "TIP: Run the command `brew info ruby` and follow the instructions to set the environment variables sLDFLAGS, CPPFLAGS, and PKG_CONFIG_PATH.", ); } if (process.env?.CDXGEN_IN_CONTAINER === "true") { console.log( "Are there any devel packages that could be included in the cdxgen container image to avoid these errors? Start a discussion thread here: https://github.com/CycloneDX/cdxgen/discussions", ); } else { console.log( `TIP: Try using the custom container image "ghcr.io/cyclonedx/cdxgen-debian-ruby34" with the argument "-t ruby${rubyVersion}"`, ); } } return { fullToolBinDir, status: result.status === 0 }; } /** * Method to install bundler using gem. * * @param rubyVersion Ruby version * @param bundlerVersion Bundler version */ export function installRubyBundler(rubyVersion, bundlerVersion) { const minRubyVersion = "3.1.0"; let bundlerWarningShown = false; if (!bundlerVersion && compareLoose(rubyVersion, minRubyVersion) === -1) { console.log( `Default installation for bundler requires Ruby >= ${minRubyVersion}. Attempting to detect and install an older version of bundler for Ruby ${rubyVersion}.`, ); bundlerWarningShown = true; } const fullToolBinDir = rubyVersionDir(rubyVersion); if (safeExistsSync(fullToolBinDir)) { const gemInstallArgs = ["install", "bundler"]; if (bundlerVersion) { gemInstallArgs.push("-v"); gemInstallArgs.push(bundlerVersion); } if (!bundlerWarningShown) { if (bundlerVersion) { console.log( `Installing bundler ${bundlerVersion} using ${join(fullToolBinDir, "gem")}`, ); } else { console.log( `Installing bundler using ${join(fullToolBinDir, "gem")} ${gemInstallArgs.join(" ")}`, ); } } const result = spawnSync(join(fullToolBinDir, "gem"), gemInstallArgs, { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, }); if (bundlerWarningShown) { if (result.stderr?.includes("Try installing it with")) { const oldBundlerVersion = result.stderr .split("`\n")[0] .split("Try installing it with `") .pop() .split(" ") .pop() .replaceAll("`", ""); if (/^\d+/.test(oldBundlerVersion)) { console.log( `The last version of bundler to support your Ruby & RubyGems was ${oldBundlerVersion}. cdxgen will now attempt to install this version.`, ); return installRubyBundler(rubyVersion, oldBundlerVersion); } } } else { if (result.error || result.status !== 0) { if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } } return result.status === 0; } } return false; } /** * Method to perform bundle install * * @param cdxgenGemHome cdxgen Gem home * @param rubyVersion Ruby version * @param bundleCommand Bundle command to use * @param basePath working directory * * @returns {boolean} true if the install was successful. false otherwise. */ export function performBundleInstall( cdxgenGemHome, rubyVersion, bundleCommand, basePath, ) { if (arch() !== "x64") { console.log( `INFO: Many Ruby packages have limited support for ${arch()} architecture. Run the cdxgen container image with --platform=linux/amd64 for best experience.`, ); } let installArgs = ["install"]; if (process.env.BUNDLE_INSTALL_ARGS) { installArgs = installArgs.concat( process.env.BUNDLE_INSTALL_ARGS.split(" "), ); } const gemFileLock = join(basePath, "Gemfile.lock"); console.log( `Invoking ${bundleCommand} ${installArgs.join(" ")} from ${basePath} with GEM_HOME ${cdxgenGemHome}. Please wait ...`, ); let result = spawnSync(bundleCommand, installArgs, { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, cwd: basePath, env: { ...process.env, GEM_HOME: cdxgenGemHome, }, }); if (result.error || result.status !== 0) { let pythonWarningShown = false; let rubyVersionWarningShown = false; if ( result?.stderr?.includes("requires python 2 to be installed") || result?.stdout?.includes("requires python 2 to be installed") ) { pythonWarningShown = true; console.log( "A native module requires python 2 to be installed. Please install python 2.7.18 from https://www.python.org/downloads/release/python-2718/.", ); console.log( "NOTE: Python 2.7.x has now reached end-of-life. Python 2.7.18, is the FINAL RELEASE of Python 2.7.x. It will no longer be supported or updated. You should stop using this project in production and decommission immediately.", ); console.log( "Further, the project might need older versions of gcc and other build tools which might not be readily available in this environment.", ); if (process.env?.CDXGEN_IN_CONTAINER === "true") { console.log( "cdxgen container images do not bundle Python 2. Run cdxgen in cli mode to proceed with the SBOM generation.", ); } console.log( "Alternatively, ensure Gemfile.lock is present locally and invoke cdxgen with the argument `--lifecycle pre-build`.", ); } if ( result?.stderr?.includes("Running `bundle update ") || result?.stdout?.includes("Running `bundle update ") ) { console.log( "Gemfile.lock appears to be outdated. Attempting automated update.", ); const packageToUpdate = result.stderr .split("Running `bundle update ") .pop() .split("`")[0]; let updateArgs = ["update"]; if (packageToUpdate?.length && !packageToUpdate.includes(" ")) { updateArgs.push(packageToUpdate); } if (process.env.BUNDLE_UPDATE_ARGS) { updateArgs = updateArgs.concat( process.env.BUNDLE_UPDATE_ARGS.split(" "), ); } console.log(`${bundleCommand} ${updateArgs.join(" ")}`); result = spawnSync(bundleCommand, updateArgs, { encoding: "utf-8", shell: isWin, timeout: TIMEOUT_MS, cwd: basePath, env: { ...process.env, GEM_HOME: cdxgenGemHome, }, }); if (result.error || result.status !== 0) { console.log("------------"); if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } console.log("------------"); } return result.status === 0; } if ( result?.stderr?.includes("Your Ruby version is ") || result?.stdout?.includes("Your Ruby version is ") ) { console.log( "This project requires a specific version of Ruby. The version requirements can be found in the error message below.", ); rubyVersionWarningShown = true; } if (result?.stderr?.includes("requires rubygems version")) { console.log( "This project requires a specific version of RubyGems. To do this, the existing version must be uninstalled followed by installing the required version. `sudo gem uninstall rubygems-update -v <existing version>` and then `sudo gem install rubygems-update -v <required version>`.", ); rubyVersionWarningShown = true; if (safeExistsSync(gemFileLock)) { console.log("Run `bundle install` command to troubleshoot the build."); } else { console.log( "Try building this project directly and set the environment variable CDXGEN_GEM_HOME with the gems directory. Look for any Dockerfile or CI workflow files for information regarding the exact version of Ruby, RubyGems, Bundler needed to build this project.", ); } if (process.env?.CDXGEN_IN_CONTAINER === "true") { console.log( "TIP: Create your own container image by using an existing Ruby base image from here: https://github.com/CycloneDX/cdxgen/tree/master/ci/base-images/debian", ); } } if (result?.stderr?.includes("Bundler cannot continue")) { console.log( 'Bundle install is unable to continue due to a dependency resolution and build issue. Running bundle install without certain groups might work in such instances. Try running cdxgen with the environment variable `BUNDLE_INSTALL_ARGS`. Example: to skip `test` group, set the variable `"BUNDLE_INSTALL_ARGS=--without test"`', ); console.log( "NOTE: The generated SBOM would be incomplete with this workaround.", ); } if (result?.stderr?.includes("Target architecture x64 is only supported")) { console.log( "A gem native extension requires x64/amd64 architecture. Run the cdxgen container image with the argument '--platform=linux/amd64'.", ); } if ( !pythonWarningShown && (result?.stderr?.includes("Failed to build gem native extension") || result?.stderr?.includes("Gem::Ext::BuildError")) ) { console.log( "Bundler failed to build some gem native extension(s). Carefully review the below error to install any required development libraries.", ); } console.log("------------"); if (result.stdout) { console.log(result.stdout); } if (result.stderr) { console.log(result.stderr); } console.log("------------"); if ( process.env?.CDXGEN_IN_CONTAINER === "true" && !rubyVersionWarningShown ) { console.log( "Are there any devel packages that could be included in the cdxgen container image to avoid these errors? Start a discussion thread here: https://github.com/CycloneDX/cdxgen/discussions", ); console.log("------------"); } else if (rubyVersion) { console.log( `TIP: Try using the custom container image "ghcr.io/cyclonedx/cdxgen-debian-ruby34" with the argument "-t ruby${rubyVersion}".`, ); } else { console.log( `TIP: Try invoking cdxgen with a Ruby version type. With the custom container image "ghcr.io/cyclonedx/cdxgen-debian-ruby34", you can pass the argument "-t ruby<version>". Example: "-t ruby3.3.6"`, ); } } return result.status === 0; }