Repository: sw360/capycli (language: python)

Issue:
"project CreateBom" only supports one source attachment per release
Ideally, there should only be one source attachment in SW360 for each release, but in practice, people tend to add more, so we should also reflect this in "project CreateBom"

Currently, this is not possible because [`create_project_bom()`](https://github.com/sw360/capycli/blob/v2.0.0/capycli/project/create_bom.py#L58) today builds up a BOM in legacy format first which only supported one `SourceFile` per release. As this is immediately converted to cdx format afterwards, I suggest to refactor the method to directly  build up a cdx BOM where I can easily store multiple sources as externalReferences.

Any objections, @tngraf?


Discussion / hints:

Fix patch:
diff --git a/ChangeLog.md b/ChangeLog.md
index 2591013..9a32132 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -10,7 +10,8 @@
 * Be more resilient about missing metadata in CycloneDX SBOMs.
 * The `-o` parameter of the command `project GetLicenseInfo` is now optional.
   But you still need this output when you want to create a Readme.
-* `project createbom` uses source url and purl from SW360 if available
+* `project createbom` add purl, source and repository url from SW360 if available
+* `project createbom` adds first SW360 source attachment of each releases as external reference to SBOM.
 * `project createbom` adds SW360 project name, version and description to SBOM.
 
 ## 2.0.0 (2023-06-02)
diff --git a/capycli/project/create_bom.py b/capycli/project/create_bom.py
index 026b2af..d46f6a5 100644
--- a/capycli/project/create_bom.py
+++ b/capycli/project/create_bom.py
@@ -95,17 +95,17 @@ def create_project_bom(self, project) -> list:
                     rel_item["RepositoryType"] = "package-url"
 
                 if "repository" in release_details:
-                    rel_item["Repository"] = release_details["repository"].get("url", "")
+                    rel_item["RepositoryUrl"] = release_details["repository"].get("url", "")
 
                 source_attachment = self.get_attachment("SOURCE", release_details)
                 if source_attachment:
-                    rel_item["SourceCodeFile"] = source_attachment.get("filename", "")
-                    rel_item["SourceCodeFileSha1"] = source_attachment.get("sha1", "")
+                    rel_item["SourceFile"] = source_attachment.get("filename", "")
+                    rel_item["SourceFileHash"] = source_attachment.get("sha1", "")
 
                 binary_attachment = self.get_attachment("BINARY", release_details)
                 if binary_attachment:
                     rel_item["BinaryFile"] = binary_attachment.get("filename", "")
-                    rel_item["BinarySha1"] = binary_attachment.get("sha1", "")
+                    rel_item["BinaryFileHash"] = binary_attachment.get("sha1", "")
 
             except sw360.SW360Error as swex:
                 print_red("    ERROR: unable to access project:" + repr(swex))


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 4bcf1dc..5b9c5ef 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -365,6 +365,10 @@ def get_release_cli_for_test() -> dict:
                 "package-url": "pkg:pypi/cli-support@1.3"
             },
             "createdOn": "2023-03-14",
+            "repository": {
+                "url": "https://github.com/sw360/clipython.git",
+                "repositorytype": "GIT"
+            },
             "mainlineState": "SPECIFIC",
             "clearingState": "APPROVED",
             "createdBy": "thomas.graf@siemens.com",
diff --git a/tests/test_create_bom.py b/tests/test_create_bom.py
index d50c3b6..b0849de 100644
--- a/tests/test_create_bom.py
+++ b/tests/test_create_bom.py
@@ -170,9 +170,23 @@ def test_project_by_id(self):
         cdx_bom = sut.create_project_cdx_bom("p001")
         cx_comp = cdx_bom.components[0]
         self.assertEqual(cx_comp.purl, release["externalIds"]["package-url"])
-        self.assertEqual(cx_comp.external_references[0].url, release["sourceCodeDownloadurl"])
-        self.assertEqual(cx_comp.external_references[0].type, ExternalReferenceType.DISTRIBUTION)
-        self.assertEqual(cx_comp.external_references[0].comment, CaPyCliBom.SOURCE_URL_COMMENT)
+
+        ext_refs_src_url = [e for e in cx_comp.external_references if e.comment == CaPyCliBom.SOURCE_URL_COMMENT]
+        self.assertEqual(len(ext_refs_src_url), 1)
+        self.assertEqual(ext_refs_src_url[0].url, release["sourceCodeDownloadurl"])
+        self.assertEqual(ext_refs_src_url[0].type, ExternalReferenceType.DISTRIBUTION)
+
+        ext_refs_src_file = [e for e in cx_comp.external_references if e.comment == CaPyCliBom.SOURCE_FILE_COMMENT]
+        self.assertEqual(len(ext_refs_src_file), 1)
+        self.assertEqual(ext_refs_src_file[0].url, release["_embedded"]["sw360:attachments"][0]["filename"])
+        self.assertEqual(ext_refs_src_file[0].type, ExternalReferenceType.DISTRIBUTION)
+        self.assertEqual(ext_refs_src_file[0].hashes[0].alg, "SHA-1")
+        self.assertEqual(ext_refs_src_file[0].hashes[0].content, release["_embedded"]["sw360:attachments"][0]["sha1"])
+
+        ext_refs_vcs = [e for e in cx_comp.external_references if e.type == ExternalReferenceType.VCS]
+        self.assertEqual(len(ext_refs_vcs), 1)
+        self.assertEqual(ext_refs_vcs[0].url, release["repository"]["url"])
+
         self.assertEqual(cdx_bom.metadata.component.name, project["name"])
         self.assertEqual(cdx_bom.metadata.component.version, project["version"])
         self.assertEqual(cdx_bom.metadata.component.description, project["description"])