diff --git a/.github/workflows/repotests.yml b/.github/workflows/repotests.yml index 3884eb7ea..cec16f175 100644 --- a/.github/workflows/repotests.yml +++ b/.github/workflows/repotests.yml @@ -293,6 +293,16 @@ jobs: repository: 'bionomia/bionomia' path: 'repotests/bionomia' ref: '5ada8b5f4a5f68561a7195e2badc2f744dc4676e' + - uses: actions/checkout@v4 + with: + repository: 'ollama/ollama' + path: 'repotests/ollama' + ref: 'v0.5.7' + - uses: actions/checkout@v4 + with: + repository: 'caddyserver/caddy' + path: 'repotests/caddy' + ref: 'v2.9.1' - uses: dtolnay/rust-toolchain@stable - name: setup sdkman run: | @@ -415,7 +425,12 @@ jobs: - name: repotests shiftleft-go-example if: matrix.os != 'macos-15' run: | - FETCH_LICENSE=false bin/cdxgen.js -p -r -t go repotests/shiftleft-go-example -o bomresults/bom-go.json --fail-on-error --export-proto + FETCH_LICENSE=false bin/cdxgen.js -p -r -t golang repotests/shiftleft-go-example -o bomresults/bom-go.json --fail-on-error --export-proto + shell: bash + - name: repotests ollama + run: | + bin/cdxgen.js -p -r -t go repotests/ollama -o bomresults/bom-ollama.json --fail-on-error + bin/cdxgen.js -p -r -t go repotests/caddy -o bomresults/bom-caddy.json --fail-on-error shell: bash - name: repotests go mod tests run: | @@ -582,7 +597,7 @@ jobs: bin/cdxgen.js -r -t java repotests/shiftleft-java-example -o bomresults/1.6-bom-java.json --generate-key-and-sign --spec-version 1.6 SBOM_SIGN_ALGORITHM=RS512 SBOM_SIGN_PRIVATE_KEY=bomresults/private.key SBOM_SIGN_PUBLIC_KEY=bomresults/public.key bin/cdxgen.js -p -r -t github repotests/shiftleft-java-example -o bomresults/1.6-bom-github.json --spec-version 1.6 FETCH_LICENSE=0 bin/cdxgen.js -r -t js repotests/shiftleft-ts-example -o bomresults/1.6-bom-ts-1.json --fail-on-error --spec-version 1.6 - FETCH_LICENSE=1 bin/cdxgen.js -r -t js repotests/shiftleft-ts-example --required-only -o bomresults/1.6-bom-ts-2.json --fail-on-error --spec-version 1.6 + FETCH_LICENSE=1 bin/cdxgen.js -r -t javascript repotests/shiftleft-ts-example --required-only -o bomresults/1.6-bom-ts-2.json --fail-on-error --spec-version 1.6 FETCH_LICENSE=true bin/cdxgen.js -r -t csharp repotests/vulnerable_net_core -o bomresults/1.6-bom-csharp2.json --spec-version 1.6 FETCH_LICENSE=false bin/cdxgen.js -r repotests/Goatly.NET -o bomresults/1.6-bom-csharp3.json --spec-version 1.6 FETCH_LICENSE=true bin/cdxgen.js -r -t python repotests/DjanGoat -o bomresults/1.6-bom-python.json --fail-on-error --spec-version 1.6 @@ -593,7 +608,7 @@ jobs: bin/cdxgen.js -r -t java repotests/shiftleft-java-example -o bomresults/1.4-bom-java.json --generate-key-and-sign --spec-version 1.4 SBOM_SIGN_ALGORITHM=RS512 SBOM_SIGN_PRIVATE_KEY=bomresults/private.key SBOM_SIGN_PUBLIC_KEY=bomresults/public.key bin/cdxgen.js -p -r -t github repotests/shiftleft-java-example -o bomresults/1.4-bom-github.json --spec-version 1.4 FETCH_LICENSE=0 bin/cdxgen.js -r -t js repotests/shiftleft-ts-example -o bomresults/1.4-bom-ts-1.json --fail-on-error --spec-version 1.4 - FETCH_LICENSE=1 bin/cdxgen.js -r -t js repotests/shiftleft-ts-example --required-only -o bomresults/1.4-bom-ts-2.json --fail-on-error --spec-version 1.4 + FETCH_LICENSE=1 bin/cdxgen.js -r -t javascript repotests/shiftleft-ts-example --required-only -o bomresults/1.4-bom-ts-2.json --fail-on-error --spec-version 1.4 FETCH_LICENSE=true bin/cdxgen.js -r -t csharp repotests/vulnerable_net_core -o bomresults/1.4-bom-csharp2.json --spec-version 1.4 FETCH_LICENSE=false bin/cdxgen.js -r repotests/Goatly.NET -o bomresults/1.4-bom-csharp3.json --spec-version 1.4 FETCH_LICENSE=true bin/cdxgen.js -r -t python repotests/DjanGoat -o bomresults/1.4-bom-python.json --fail-on-error --spec-version 1.4 diff --git a/bin/cdxgen.js b/bin/cdxgen.js index 8f288fb21..b3b07b0ee 100755 --- a/bin/cdxgen.js +++ b/bin/cdxgen.js @@ -388,7 +388,7 @@ if (process.env.GLOBAL_AGENT_HTTP_PROXY || process.env.HTTP_PROXY) { globalAgent.bootstrap(); } -const filePath = args._[0] || process.cwd(); +const filePath = resolve(args._[0]) || process.cwd(); if (!args.projectName) { if (filePath !== ".") { args.projectName = basename(filePath); @@ -411,6 +411,10 @@ const options = Object.assign({}, args, { noBabel: args.noBabel || args.babel === false, project: args.projectId, deep: args.deep || args.evidence, + output: + isSecureMode && args.output === "bom.json" + ? resolve(join(filePath, args.output)) + : args.output, }); if (process.argv[1].includes("cbom")) { @@ -419,6 +423,13 @@ if (process.argv[1].includes("cbom")) { options.specVersion = 1.6; options.deep = true; } +if (process.argv[1].includes("cdxgen-secure")) { + console.log( + "NOTE: Secure mode only restricts cdxgen from performing certain activities such as package installation. It does not provide security guarantees in the presence of malicious code.", + ); + options.installDeps = false; + process.env.CDXGEN_SECURE_MODE = true; +} if (options.standard) { options.specVersion = 1.6; } @@ -542,10 +553,29 @@ applyAdvancedOptions(options); * @returns */ const checkPermissions = (filePath, options) => { + const fullFilePath = resolve(filePath); + if (process.getuid() === 0 && process.env?.CDXGEN_IN_CONTAINER !== "true") { + console.log( + "\x1b[1;35mSECURE MODE: DO NOT run cdxgen with root privileges.\x1b[0m", + ); + } if (!process.permission) { + if (isSecureMode) { + console.log( + "\x1b[1;35mSecure mode requires permission-related arguments. These can be passed as CLI arguments directly to the node runtime or set the NODE_OPTIONS environment variable as shown below.\x1b[0m", + ); + const nodeOptionsVal = `--permission --allow-fs-read="${getTmpDir()}/*" --allow-fs-write="${getTmpDir()}/*" --allow-fs-read="${fullFilePath}/*" --allow-fs-write="${options.output}" --allow-child-process`; + console.log( + `${isWin ? "$env:" : "export "}NODE_OPTIONS='${nodeOptionsVal}'`, + ); + if (process.env?.CDXGEN_IN_CONTAINER !== "true") { + console.log( + "TIP: Run cdxgen using the secure container image 'ghcr.io/cyclonedx/cdxgen-secure' for best experience.", + ); + } + } return true; } - const fullFilePath = resolve(filePath); // Secure mode checks if (isSecureMode) { if (process.permission.has("fs.read", "*")) { @@ -565,7 +595,7 @@ const checkPermissions = (filePath, options) => { } if (filePath !== fullFilePath) { console.log( - `\x1b[1;35mSECURE MODE: Invoke cdxgen with an absolute path to improve security. Use ${fullFilePath} instead of ${filePath}.\x1b[0m`, + `\x1b[1;35mSECURE MODE: Invoke cdxgen with an absolute path to improve security. Use '${fullFilePath}' instead of '${filePath}'\x1b[0m`, ); if (fullFilePath.includes(" ")) { console.log( diff --git a/lib/helpers/validator.js b/lib/helpers/validator.js index 511de56a1..c0ac9aea9 100644 --- a/lib/helpers/validator.js +++ b/lib/helpers/validator.js @@ -279,13 +279,18 @@ export function validateProps(bomJson) { const errorList = []; const warningsList = []; let isWorkspaceMode = false; + let lacksProperties = false; + let lacksEvidence = false; if (bomJson?.components) { for (const comp of bomJson.components) { if (!["library", "framework"].includes(comp.type)) { continue; } if (!comp.properties) { - warningsList.push(`${comp["bom-ref"]} lacks properties.`); + if (!lacksProperties) { + warningsList.push(`${comp["bom-ref"]} lacks properties.`); + lacksProperties = true; + } } else { let srcFilePropFound = false; let workspacePropFound = false; @@ -312,7 +317,8 @@ export function validateProps(bomJson) { warningsList.push(`${comp["bom-ref"]} lacks SrcFile property.`); } } - if (!comp.evidence) { + if (!comp.evidence && !lacksEvidence) { + lacksEvidence = true; warningsList.push(`${comp["bom-ref"]} lacks evidence.`); } } diff --git a/lib/server/server.js b/lib/server/server.js index 856859469..cb5138188 100644 --- a/lib/server/server.js +++ b/lib/server/server.js @@ -8,7 +8,7 @@ import url from "node:url"; import bodyParser from "body-parser"; import connect from "connect"; import { createBom, submitBom } from "../cli/index.js"; -import { getTmpDir } from "../helpers/utils.js"; +import { getTmpDir, isSecureMode } from "../helpers/utils.js"; import { postProcess } from "../stages/postgen/postgen.js"; import compression from "compression"; @@ -131,7 +131,32 @@ const configureServer = (cdxgenServer) => { }; const start = (options) => { - console.log("Listening on", options.serverHost, options.serverPort); + console.log( + "Listening on", + options.serverHost, + options.serverPort, + "without authentication!", + ); + if (["0.0.0.0", "::", "::/128", "::/0"].includes(options.serverHost)) { + console.log("Exposing cdxgen server on all IP address is a security risk!"); + if (isSecureMode) { + process.exit(1); + } + } + if (+options.serverPort < 1024) { + console.log( + "Running cdxgen server with a privileged port is a security risk!", + ); + if (isSecureMode) { + process.exit(1); + } + } + if (process.getuid() === 0 && process.env?.CDXGEN_IN_CONTAINER !== "true") { + console.log("Running cdxgen server as root is a security risk!"); + if (isSecureMode) { + process.exit(1); + } + } const cdxgenServer = http .createServer(app) .listen(options.serverPort, options.serverHost); @@ -163,11 +188,29 @@ const start = (options) => { if (filePath.startsWith("http") || filePath.startsWith("git")) { srcDir = gitClone(filePath, reqOptions.gitBranch); cleanup = true; + } else if (srcDir !== path.resolve(srcDir)) { + console.log( + `Invoke the API with an absolute path '${path.resolve(srcDir)}' instead of '${srcDir}' to reduce security risks.`, + ); + if (isSecureMode) { + res.writeHead(500, { "Content-Type": "application/json" }); + return res.end( + JSON.stringify({ + error: `Provide an absolute path instead of '${srcDir}'`, + details: "Relative paths are not supported in secure mode.", + }), + ); + } } console.log("Generating SBOM for", srcDir); let bomNSData = (await createBom(srcDir, reqOptions)) || {}; bomNSData = postProcess(bomNSData, reqOptions); if (reqOptions.serverUrl && reqOptions.apiKey) { + if (isSecureMode && !reqOptions.serverUrl?.startsWith("https://")) { + console.log( + "Dependency Track API server is used with a non-https url, which poses a security risk.", + ); + } console.log( `Publishing SBOM ${reqOptions.projectName} to Dependency Track`, reqOptions.serverUrl, diff --git a/package.json b/package.json index 525c1bf19..1b55118bd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "types": "./types/index.d.ts", "bin": { "cdxgen": "bin/cdxgen.js", + "cdxgen-secure": "bin/cdxgen.js", "obom": "bin/cdxgen.js", "cbom": "bin/cdxgen.js", "cdxi": "bin/repl.js", diff --git a/types/lib/helpers/validator.d.ts.map b/types/lib/helpers/validator.d.ts.map index 3b38ac9c9..8385afe78 100644 --- a/types/lib/helpers/validator.d.ts.map +++ b/types/lib/helpers/validator.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../lib/helpers/validator.js"],"names":[],"mappings":"AAgRA;;;;GAIG;AACH,uCAFW,MAAM,WAqDhB;AArTM,qCAFI,MAAM,WAgDhB;AAOM,0CAFI,MAAM,WAwDhB;AAOM,uCAFI,MAAM,WAgEhB;AA6BM,sCAFI,MAAM,WAgDhB"} \ No newline at end of file +{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../lib/helpers/validator.js"],"names":[],"mappings":"AAgRA;;;;GAIG;AACH,uCAFW,MAAM,WA2DhB;AA3TM,qCAFI,MAAM,WAgDhB;AAOM,0CAFI,MAAM,WAwDhB;AAOM,uCAFI,MAAM,WAgEhB;AA6BM,sCAFI,MAAM,WAgDhB"} \ No newline at end of file diff --git a/types/lib/server/server.d.ts.map b/types/lib/server/server.d.ts.map index c0b66d9b9..8437e9455 100644 --- a/types/lib/server/server.d.ts.map +++ b/types/lib/server/server.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../lib/server/server.js"],"names":[],"mappings":"AA6HA,yDAKC;AAED,0CAuEC"} \ No newline at end of file +{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../lib/server/server.js"],"names":[],"mappings":"AA6HA,yDAKC;AAED,0CAkHC"} \ No newline at end of file