Repository: sw360/capycli (language: python)

Issue:
"project createbom": multiple purls stored encoded as qualifiers
As we switched to newer versions of the cylconedx library a while ago, multiple package URLs which I used to store as a space separated list (violating the CycloneDX spec), are now stored in a strange encoded way. 

The PackageURL library considers the subsequent purls to be part of the qualifier:

```json
    {
      "type": "library",
      "bom-ref": "pkg:deb/debian/grub2@2.02%2Bdfsg1-20?arch=source pkg:deb/debian/grub-efi-amd64-signed@1%2B2.02%2Bdfsg1%2B20?arch=source",
      "name": "grub2",
      "version": "2.02+dfsg1-20.debian",
      "purl": "pkg:deb/debian/grub2@2.02%2Bdfsg1-20?arch=source%20pkg:deb/debian/grub-efi-amd64-signed%401%2B2.02%2Bdfsg1%2B20%3Farch%3Dsource"
    }
```

This is hard to fix and parse later, for both humans and code.

PR will follow in a minute.


Discussion / hints:

Fix patch:
diff --git a/ChangeLog.md b/ChangeLog.md
index 004f9b1..624d589 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -8,6 +8,8 @@
 ## NEXT
 
 * make `findsources` more resilient against SW360 issues.
+* `project createbom` now stores multiple purls in the property "purl_list" instead of
+  trying to encode them in a strange way in the "purl" field.
 
 ## 2.5.1 (2024-10-16)
 
diff --git a/capycli/project/create_bom.py b/capycli/project/create_bom.py
index 6828fbd..775c7d3 100644
--- a/capycli/project/create_bom.py
+++ b/capycli/project/create_bom.py
@@ -69,16 +69,15 @@ def create_project_bom(self, project: Dict[str, Any]) -> List[Component]:
                     purl = self.get_external_id("purl", release_details)
 
                 purls = PurlUtils.parse_purls_from_external_id(purl)
-                if len(purls) > 1:
-                    print_yellow("      Multiple purls added for", release["name"], release["version"])
-                    print_yellow("      You must remove all but one in your SBOM!")
-                purl = " ".join(purls).strip()
-
-                if purl:
-                    rel_item = Component(name=release["name"], version=release["version"],
-                                         purl=PackageURL.from_string(purl), bom_ref=purl)
-                else:
+                if len(purls) != 1:
                     rel_item = Component(name=release["name"], version=release["version"])
+                    if len(purls) > 1:
+                        print_yellow("      Multiple purls for", release["name"], release["version"])
+                        print_yellow("      Stored them in property purl_list in your SBOM!")
+                        CycloneDxSupport.set_property(rel_item, "purl_list", " ".join(purls))
+                elif len(purls) == 1:
+                    rel_item = Component(name=release["name"], version=release["version"],
+                                         purl=PackageURL.from_string(purls[0]), bom_ref=purls[0])
 
                 for key, property in (("clearingState", CycloneDxSupport.CDX_PROP_CLEARING_STATE),
                                       ("mainlineState", CycloneDxSupport.CDX_PROP_REL_STATE)):


Test patch:
diff --git a/tests/test_create_bom.py b/tests/test_create_bom.py
index 8ce47b9..22e50c0 100644
--- a/tests/test_create_bom.py
+++ b/tests/test_create_bom.py
@@ -13,7 +13,7 @@
 import responses
 from cyclonedx.model import ExternalReferenceType
 
-from capycli.common.capycli_bom_support import CaPyCliBom
+from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
 from capycli.main.result_codes import ResultCode
 from capycli.project.create_bom import CreateBom
 from tests.test_base import AppArguments, TestBasePytest
@@ -153,10 +153,10 @@ def test_create_bom_multiple_purls(self, capsys: Any) -> None:
         cdx_components = sut.create_project_bom(self.get_project_for_test())
         captured = capsys.readouterr()
 
-        assert "Multiple purls added" in captured.out
-        assert cdx_components[0].purl is not None
-        if cdx_components[0].purl:
-            assert cdx_components[0].purl.to_string() == "pkg:deb/debian/cli-support%401.3-1%20pkg:pypi/cli-support@1.3"
+        assert "Stored them in property purl_list" in captured.out
+        assert cdx_components[0].purl is None
+        purl_raw = CycloneDxSupport.get_property(cdx_components[0], "purl_list").value
+        assert purl_raw == "pkg:deb/debian/cli-support@1.3-1 pkg:pypi/cli-support@1.3"
 
     @responses.activate
     def test_project_by_id(self) -> None: