diff --git a/lib/cli/index.js b/lib/cli/index.js index 874194b8f9..6bd80471a0 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -4884,16 +4884,34 @@ export function createCppBom(path, options) { cmakeLikeFiles = cmakeLikeFiles.concat(cmakeFiles); } let pkgList = []; + let parentComponentDependencies = []; if (conanLockFiles.length) { for (const f of conanLockFiles) { if (DEBUG_MODE) { console.log(`Parsing ${f}`); } const conanLockData = readFileSync(f, { encoding: "utf-8" }); - const dlist = parseConanLockData(conanLockData); - if (dlist?.length) { - pkgList = pkgList.concat(dlist); + const { + pkgList: conanPkgList, + dependencies: conanDependencies, + parentComponentDependencies: parentCompDeps, + } = parseConanLockData(conanLockData); + + if (conanPkgList.length) { + pkgList = pkgList.concat(conanPkgList); + } + + if (Object.keys(conanDependencies).length) { + dependencies = mergeDependencies( + dependencies, + Object.keys(conanDependencies).map((dependentBomRef) => ({ + ref: dependentBomRef, + dependsOn: conanDependencies[dependentBomRef], + })), + ); } + + parentComponentDependencies = parentCompDeps; } } else if (conanFiles.length) { for (const f of conanFiles) { @@ -5015,6 +5033,16 @@ export function createCppBom(path, options) { } options.parentComponent = parentComponent; } + + if (parentComponent && parentComponentDependencies.length) { + dependencies = mergeDependencies(dependencies, [ + { + ref: parentComponent["bom-ref"], + dependsOn: parentComponentDependencies, + }, + ]); + } + return buildBomNSData(options, pkgList, "generic", { src: path, parentComponent, diff --git a/lib/helpers/utils.js b/lib/helpers/utils.js index 1d53846734..19c658984b 100644 --- a/lib/helpers/utils.js +++ b/lib/helpers/utils.js @@ -10013,29 +10013,67 @@ export function mapConanPkgRefToPurlStringAndNameAndVersion(conanPkgRef) { export function parseConanLockData(conanLockData) { const pkgList = []; + const dependencies = {}; + const parentComponentDependencies = []; + if (!conanLockData) { - return pkgList; + return { pkgList, dependencies, parentComponentDependencies }; } + const lockFile = JSON.parse(conanLockData); if ( (!lockFile || !lockFile.graph_lock || !lockFile.graph_lock.nodes) && !lockFile.requires ) { - return pkgList; + return { pkgList, dependencies, parentComponentDependencies }; } + if (lockFile.graph_lock?.nodes) { const depends = lockFile.graph_lock.nodes; + const nodeKeyToBomRefMap = {}; + for (const nk of Object.keys(depends)) { - if (depends[nk].ref) { - const [purl, name, version] = - mapConanPkgRefToPurlStringAndNameAndVersion(depends[nk].ref); - if (purl !== null) { - pkgList.push({ - name, - version, - purl, - "bom-ref": decodeURIComponent(purl), - }); + if (!depends[nk].ref) continue; + + const [purl, name, version] = mapConanPkgRefToPurlStringAndNameAndVersion( + depends[nk].ref, + ); + if (purl === null) continue; + + const bomRef = decodeURIComponent(purl); + pkgList.push({ + name, + version, + purl, + "bom-ref": bomRef, + }); + + nodeKeyToBomRefMap[nk] = bomRef; + } + + for (const nk of Object.keys(depends)) { + let requirementNodeKeys = []; + if (Array.isArray(depends[nk].requires)) + requirementNodeKeys = requirementNodeKeys.concat(depends[nk].requires); + if (Array.isArray(depends[nk].build_requires)) + requirementNodeKeys = requirementNodeKeys.concat( + depends[nk].build_requires, + ); + + for (const dependencyNodeKey of requirementNodeKeys) { + const dependencyBomRef = nodeKeyToBomRefMap[dependencyNodeKey]; + if (!dependencyBomRef) continue; + + const dependentBomRef = nodeKeyToBomRefMap[nk]; + if (dependentBomRef) { + if (!(dependentBomRef in dependencies)) + dependencies[dependentBomRef] = []; + if (!dependencies[dependentBomRef].includes(dependencyBomRef)) + dependencies[dependentBomRef].push(dependencyBomRef); + } else if (nk === "0") { + // parent component for which the conan.lock was generated + if (!parentComponentDependencies.includes(dependencyBomRef)) + parentComponentDependencies.push(dependencyBomRef); } } } @@ -10056,7 +10094,7 @@ export function parseConanLockData(conanLockData) { } } } - return pkgList; + return { pkgList, dependencies, parentComponentDependencies }; } export function parseConanData(conanData) { diff --git a/lib/helpers/utils.poku.js b/lib/helpers/utils.poku.js index 672916c197..90863132f6 100644 --- a/lib/helpers/utils.poku.js +++ b/lib/helpers/utils.poku.js @@ -2185,28 +2185,144 @@ it("parse cabal freeze", () => { }); it("parse conan data", () => { - assert.deepStrictEqual(parseConanLockData(null), []); - let dep_list = parseConanLockData( + let conanLockData = parseConanLockData(null); + assert.deepStrictEqual(conanLockData.pkgList.length, 0); + assert.deepStrictEqual(Object.keys(conanLockData.dependencies).length, 0); + assert.deepStrictEqual(conanLockData.parentComponentDependencies.length, 0); + conanLockData = parseConanLockData( readFileSync("./test/data/conan-v1.lock", { encoding: "utf-8" }), ); - assert.deepStrictEqual(dep_list.length, 3); - assert.deepStrictEqual(dep_list[0], { + assert.deepStrictEqual(conanLockData.pkgList.length, 3); + assert.deepStrictEqual(conanLockData.pkgList[0], { name: "zstd", version: "1.4.4", "bom-ref": "pkg:conan/zstd@1.4.4", purl: "pkg:conan/zstd@1.4.4", }); - dep_list = parseConanLockData( + assert.deepStrictEqual(Object.keys(conanLockData.dependencies).length, 0); + assert.deepStrictEqual(conanLockData.parentComponentDependencies, [ + "pkg:conan/zstd@1.4.4", + "pkg:conan/jerryscript@2.2.0", + "pkg:conan/wolfssl@4.4.0", + ]); + + conanLockData = parseConanLockData( + readFileSync("./test/data/conan-v1-for-reference.lock", { + encoding: "utf-8", + }), + ); + assert.deepStrictEqual(Object.keys(conanLockData.pkgList).length, 7); + assert.deepStrictEqual(conanLockData.pkgList[0], { + name: "grpc", + version: "1.50.1", + "bom-ref": "pkg:conan/grpc@1.50.1", + purl: "pkg:conan/grpc@1.50.1", + }); + assert.deepStrictEqual(Object.keys(conanLockData.dependencies).length, 3); + assert.deepStrictEqual(conanLockData.dependencies["pkg:conan/grpc@1.50.1"], [ + "pkg:conan/abseil@20230802.1", + "pkg:conan/protobuf@3.21.12", + "pkg:conan/c-ares@1.34.1", + "pkg:conan/openssl@3.3.2", + "pkg:conan/re2@20230301", + "pkg:conan/zlib@1.3.1", + ]); + assert.deepStrictEqual( + conanLockData.dependencies["pkg:conan/protobuf@3.21.12"], + ["pkg:conan/zlib@1.3.1"], + ); + assert.deepStrictEqual( + conanLockData.dependencies["pkg:conan/openssl@3.3.2"], + ["pkg:conan/zlib@1.3.1"], + ); + assert.deepStrictEqual(conanLockData.parentComponentDependencies.length, 0); + + conanLockData = parseConanLockData( + readFileSync("./test/data/conan-v1-with-nested-deps.lock", { + encoding: "utf-8", + }), + ); + assert.deepStrictEqual(conanLockData.pkgList.length, 9); + assert.deepStrictEqual(conanLockData.pkgList[0], { + name: "grpc", + version: "1.50.1", + "bom-ref": "pkg:conan/grpc@1.50.1", + purl: "pkg:conan/grpc@1.50.1", + }); + assert.deepStrictEqual(conanLockData.pkgList[1], { + name: "abseil", + version: "20230802.1", + "bom-ref": "pkg:conan/abseil@20230802.1", + purl: "pkg:conan/abseil@20230802.1", + }); + assert.deepStrictEqual(conanLockData.pkgList[2], { + name: "protobuf", + version: "3.21.12", + "bom-ref": "pkg:conan/protobuf@3.21.12", + purl: "pkg:conan/protobuf@3.21.12", + }); + assert.deepStrictEqual(conanLockData.pkgList[3], { + name: "zlib", + version: "1.3.1", + "bom-ref": "pkg:conan/zlib@1.3.1", + purl: "pkg:conan/zlib@1.3.1", + }); + assert.deepStrictEqual(conanLockData.pkgList[5], { + name: "openssl", + version: "3.3.2", + "bom-ref": "pkg:conan/openssl@3.3.2", + purl: "pkg:conan/openssl@3.3.2", + }); + assert.deepStrictEqual(conanLockData.pkgList[8], { + name: "gtest", + version: "1.13.0", + "bom-ref": "pkg:conan/gtest@1.13.0", + purl: "pkg:conan/gtest@1.13.0", + }); + assert.deepStrictEqual(Object.keys(conanLockData.dependencies).length, 3); + assert.deepStrictEqual(conanLockData.dependencies["pkg:conan/grpc@1.50.1"], [ + "pkg:conan/abseil@20230802.1", + "pkg:conan/protobuf@3.21.12", + "pkg:conan/c-ares@1.34.1", + "pkg:conan/openssl@3.3.2", + "pkg:conan/re2@20230301", + "pkg:conan/zlib@1.3.1", + ]); + assert.deepStrictEqual( + conanLockData.dependencies["pkg:conan/protobuf@3.21.12"], + ["pkg:conan/zlib@1.3.1"], + ); + assert.deepStrictEqual( + conanLockData.dependencies["pkg:conan/openssl@3.3.2"], + ["pkg:conan/zlib@1.3.1"], + ); + assert.deepStrictEqual(conanLockData.parentComponentDependencies.length, 3); + assert.deepStrictEqual( + conanLockData.parentComponentDependencies[0], + "pkg:conan/grpc@1.50.1", + ); + assert.deepStrictEqual( + conanLockData.parentComponentDependencies[1], + "pkg:conan/rapidjson@1.1.0", + ); + assert.deepStrictEqual( + conanLockData.parentComponentDependencies[2], + "pkg:conan/gtest@1.13.0", + ); + + conanLockData = parseConanLockData( readFileSync("./test/data/conan-v2.lock", { encoding: "utf-8" }), ); - assert.deepStrictEqual(dep_list.length, 2); - assert.deepStrictEqual(dep_list[0], { + assert.deepStrictEqual(conanLockData.pkgList.length, 2); + assert.deepStrictEqual(conanLockData.pkgList[0], { name: "opensta", version: "4.0.0", "bom-ref": "pkg:conan/opensta@4.0.0?rrev=765a7eed989e624c762a73291d712b14", purl: "pkg:conan/opensta@4.0.0?rrev=765a7eed989e624c762a73291d712b14", }); - dep_list = parseConanData( + assert.deepStrictEqual(Object.keys(conanLockData.dependencies).length, 0); + assert.deepStrictEqual(conanLockData.parentComponentDependencies.length, 0); + let dep_list = parseConanData( readFileSync("./test/data/conanfile.txt", { encoding: "utf-8" }), ); assert.deepStrictEqual(dep_list.length, 3); @@ -2316,34 +2432,34 @@ it("conan package reference mapper to pURL", () => { }); it("parse conan data where packages use custom user/channel", () => { - let dep_list = parseConanLockData( + const conanLockData = parseConanLockData( readFileSync("./test/data/conan.with_custom_pkg_user_channel.lock", { encoding: "utf-8", }), ); - assert.deepStrictEqual(dep_list.length, 4); - assert.deepStrictEqual(dep_list[0], { + assert.deepStrictEqual(conanLockData.pkgList.length, 4); + assert.deepStrictEqual(conanLockData.pkgList[0], { name: "libcurl", version: "8.1.2", "bom-ref": "pkg:conan/libcurl@8.1.2?channel=stable&rrev=25215c550633ef0224152bc2c0556698&user=internal", purl: "pkg:conan/libcurl@8.1.2?channel=stable&rrev=25215c550633ef0224152bc2c0556698&user=internal", }); - assert.deepStrictEqual(dep_list[1], { + assert.deepStrictEqual(conanLockData.pkgList[1], { name: "openssl", version: "3.1.0", "bom-ref": "pkg:conan/openssl@3.1.0?channel=stable&rrev=c9c6ab43aa40bafacf8b37c5948cdb1f&user=internal", purl: "pkg:conan/openssl@3.1.0?channel=stable&rrev=c9c6ab43aa40bafacf8b37c5948cdb1f&user=internal", }); - assert.deepStrictEqual(dep_list[2], { + assert.deepStrictEqual(conanLockData.pkgList[2], { name: "zlib", version: "1.2.13", "bom-ref": "pkg:conan/zlib@1.2.13?channel=stable&rrev=aee6a56ff7927dc7261c55eb2db4fc5b&user=internal", purl: "pkg:conan/zlib@1.2.13?channel=stable&rrev=aee6a56ff7927dc7261c55eb2db4fc5b&user=internal", }); - assert.deepStrictEqual(dep_list[3], { + assert.deepStrictEqual(conanLockData.pkgList[3], { name: "fmt", version: "10.0.0", purl: "pkg:conan/fmt@10.0.0?channel=stable&rrev=79e7cc169695bc058fb606f20df6bb10&user=internal", @@ -2351,7 +2467,7 @@ it("parse conan data where packages use custom user/channel", () => { "pkg:conan/fmt@10.0.0?channel=stable&rrev=79e7cc169695bc058fb606f20df6bb10&user=internal", }); - dep_list = parseConanData( + const dep_list = parseConanData( readFileSync("./test/data/conanfile.with_custom_pkg_user_channel.txt", { encoding: "utf-8", }), diff --git a/test/data/conan-v1-for-reference.lock b/test/data/conan-v1-for-reference.lock new file mode 100644 index 0000000000..1d5a82bfb5 --- /dev/null +++ b/test/data/conan-v1-for-reference.lock @@ -0,0 +1,72 @@ +{ + "graph_lock": { + "nodes": { + "1": { + "ref": "grpc/1.50.1", + "options": "", + "package_id": "0febd4ee2d7c3ac6dfc28d10eef3f352c517fde7", + "prev": "0", + "requires": [ + "2", + "3", + "5", + "6", + "7", + "4" + ], + "context": "host" + }, + "2": { + "ref": "abseil/20230802.1", + "options": "shared=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + }, + "3": { + "ref": "protobuf/3.21.12", + "options": "", + "package_id": "6b251a3d67c612ea01b0038b4c794e87a3c4fd88", + "prev": "0", + "requires": [ + "4" + ], + "context": "host" + }, + "4": { + "ref": "zlib/1.3.1", + "options": "shared=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + }, + "5": { + "ref": "c-ares/1.34.1", + "options": "shared=False\ntools=True", + "package_id": "95ac61b4b7e9e66bcc9af3260647ab977ee3250a", + "prev": "0", + "context": "host" + }, + "6": { + "ref": "openssl/3.3.2", + "options": "", + "package_id": "1cf626f618fdd256dd79c53f4d6cebfc2eaa1df7", + "prev": "0", + "requires": [ + "4" + ], + "context": "host" + }, + "7": { + "ref": "re2/20230301", + "options": "shared=False\nwith_icu=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "" +} \ No newline at end of file diff --git a/test/data/conan-v1-with-nested-deps.lock b/test/data/conan-v1-with-nested-deps.lock new file mode 100644 index 0000000000..a58e7109c0 --- /dev/null +++ b/test/data/conan-v1-with-nested-deps.lock @@ -0,0 +1,98 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "", + "requires": [ + "1", + "8" + ], + "build_requires": [ + "9" + ], + "path": "conanfile.txt", + "context": "host" + }, + "1": { + "ref": "grpc/1.50.1", + "options": "", + "package_id": "0febd4ee2d7c3ac6dfc28d10eef3f352c517fde7", + "prev": "0", + "requires": [ + "2", + "3", + "5", + "6", + "7", + "4" + ], + "context": "host" + }, + "2": { + "ref": "abseil/20230802.1", + "options": "shared=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + }, + "3": { + "ref": "protobuf/3.21.12", + "options": "", + "package_id": "6b251a3d67c612ea01b0038b4c794e87a3c4fd88", + "prev": "0", + "requires": [ + "4" + ], + "context": "host" + }, + "4": { + "ref": "zlib/1.3.1", + "options": "shared=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + }, + "5": { + "ref": "c-ares/1.34.1", + "options": "shared=False\ntools=True", + "package_id": "95ac61b4b7e9e66bcc9af3260647ab977ee3250a", + "prev": "0", + "context": "host" + }, + "6": { + "ref": "openssl/3.3.2", + "options": "", + "package_id": "1cf626f618fdd256dd79c53f4d6cebfc2eaa1df7", + "prev": "0", + "requires": [ + "4" + ], + "context": "host" + }, + "7": { + "ref": "re2/20230301", + "options": "shared=False\nwith_icu=False", + "package_id": "3fb49604f9c2f729b85ba3115852006824e72cab", + "prev": "0", + "context": "host" + }, + "8": { + "ref": "rapidjson/1.1.0", + "options": "", + "package_id": "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", + "prev": "0", + "context": "host" + }, + "9": { + "ref": "gtest/1.13.0", + "options": "", + "package_id": "5ad274d83035c78ba2b205e6cf4f1b317aee8e05", + "prev": "0", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "" +} \ No newline at end of file