Skip to content

Commit a08cbc2

Browse files
authored
Merge pull request #32 from sw360/feat-multiple-sources
project CreateBom: add multiple and self-made sources to SBOM
2 parents e1b56fa + 13af321 commit a08cbc2

4 files changed

Lines changed: 98 additions & 58 deletions

File tree

ChangeLog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* The `-o` parameter of the command `project GetLicenseInfo` is now optional.
1212
But you still need this output when you want to create a Readme.
1313
* `project createbom` add purl, source and repository url from SW360 if available
14-
* `project createbom` adds first SW360 source attachment of each releases as external reference to SBOM.
14+
* `project createbom` add SW360 source and binary attachments as external reference to SBOM.
1515
* `project createbom` adds SW360 project name, version and description to SBOM.
1616

1717
## 2.0.0 (2023-06-02)

capycli/common/capycli_bom_support.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ def get_property(comp: Component, name: str) -> Any:
298298

299299
return None
300300

301+
@staticmethod
302+
def set_property(comp: Component, name: str, value: str) -> None:
303+
"""Sets the property with the given name."""
304+
comp.properties.add(Property(name=name, value=value))
305+
301306
@staticmethod
302307
def update_or_set_property(comp: Component, name: str, value: str) -> None:
303308
"""Returns the property with the given name."""
@@ -310,7 +315,7 @@ def update_or_set_property(comp: Component, name: str, value: str) -> None:
310315
if prop:
311316
prop.value = value
312317
else:
313-
comp.properties.add(Property(name=name, value=value))
318+
CycloneDxSupport.set_property(comp, name, value)
314319

315320
@staticmethod
316321
def remove_property(comp: Component, name: str) -> None:
@@ -337,6 +342,21 @@ def get_ext_ref(comp: Component, type: ExternalReferenceType, comment: str) -> O
337342

338343
return None
339344

345+
@staticmethod
346+
def set_ext_ref(comp: Component, type: ExternalReferenceType, comment: str, value: str,
347+
hash_algo: str = None, hash: str = None) -> None:
348+
ext_ref = ExternalReference(
349+
reference_type=type,
350+
url=value,
351+
comment=comment)
352+
353+
if hash_algo and hash:
354+
ext_ref.hashes.add(HashType(
355+
algorithm=HashAlgorithm.SHA_1,
356+
hash_value=hash))
357+
358+
comp.external_references.add(ext_ref)
359+
340360
@staticmethod
341361
def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment: str, value: str) -> None:
342362
ext_ref = None
@@ -348,11 +368,7 @@ def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment:
348368
if ext_ref:
349369
ext_ref.url = value
350370
else:
351-
ext_ref = ExternalReference(
352-
reference_type=type,
353-
url=value,
354-
comment=comment)
355-
comp.external_references.add(ext_ref)
371+
CycloneDxSupport.set_ext_ref(comp, type, comment, value)
356372

357373
@staticmethod
358374
def get_ext_ref_by_comment(comp: Component, comment: str) -> Any:

capycli/project/create_bom.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99
import logging
1010
import sys
1111

12+
from typing import List, Tuple
13+
1214
import sw360
15+
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
1316
from cyclonedx.model.bom import Bom
17+
from cyclonedx.model.component import Component
1418

1519
import capycli.common.script_base
1620
from capycli import get_logger
17-
from capycli.bom.legacy import LegacySupport
18-
from capycli.common.capycli_bom_support import CaPyCliBom, SbomCreator
21+
from capycli.common.capycli_bom_support import CaPyCliBom, SbomCreator, CycloneDxSupport
22+
1923
from capycli.common.print import print_red, print_text, print_yellow
2024
from capycli.main.result_codes import ResultCode
2125

@@ -32,20 +36,21 @@ def get_external_id(self, name: str, release_details: dict):
3236

3337
return release_details["externalIds"].get(name, "")
3438

35-
def get_attachment(self, att_type: str, release_details: dict) -> dict:
36-
"""Returns the first attachment with the given type or None."""
39+
def get_attachments(self, att_types: Tuple[str], release_details: dict) -> List[dict]:
40+
"""Returns the attachments with the given types or empty list."""
3741
if "_embedded" not in release_details:
3842
return None
3943

4044
if "sw360:attachments" not in release_details["_embedded"]:
4145
return None
4246

47+
found = []
4348
attachments = release_details["_embedded"]["sw360:attachments"]
4449
for attachment in attachments:
45-
if attachment["attachmentType"] == att_type:
46-
return attachment
50+
if attachment["attachmentType"] in att_types:
51+
found.append(attachment)
4752

48-
return None
53+
return found
4954

5055
def get_clearing_state(self, proj, href) -> str:
5156
"""Returns the clearing state of the given component/release"""
@@ -64,53 +69,66 @@ def create_project_bom(self, project) -> list:
6469
for release in releases:
6570
print_text(" ", release["name"], release["version"])
6671
href = release["_links"]["self"]["href"]
67-
state = self.get_clearing_state(project, href)
68-
69-
rel_item = {}
70-
rel_item["Name"] = release["name"]
71-
rel_item["Version"] = release["version"]
72-
rel_item["ProjectClearingState"] = state
73-
rel_item["Id"] = self.client.get_id_from_href(href)
74-
rel_item["Sw360Id"] = rel_item["Id"]
75-
rel_item["Url"] = (
76-
self.sw360_url
77-
+ "group/guest/components/-/component/release/detailRelease/"
78-
+ self.client.get_id_from_href(href))
7972

8073
try:
8174
release_details = self.client.get_release_by_url(href)
82-
# capycli.common.json_support.print_json(release_details)
83-
rel_item["ClearingState"] = release_details["clearingState"]
84-
rel_item["ReleaseMainlineState"] = release_details.get("mainlineState", "")
8575

86-
rel_item["Language"] = self.list_to_string(release_details.get("languages", ""))
87-
rel_item["SourceFileUrl"] = release_details.get("sourceCodeDownloadurl", "")
88-
rel_item["BinaryFileUrl"] = release_details.get("binaryDownloadurl", "")
89-
90-
rel_item["RepositoryId"] = self.get_external_id("package-url", release_details)
91-
if not rel_item["RepositoryId"]:
76+
purl = self.get_external_id("package-url", release_details)
77+
if not purl:
9278
# try another id name
93-
rel_item["RepositoryId"] = self.get_external_id("purl", release_details)
94-
if rel_item["RepositoryId"]:
95-
rel_item["RepositoryType"] = "package-url"
96-
97-
if "repository" in release_details:
98-
rel_item["RepositoryUrl"] = release_details["repository"].get("url", "")
99-
100-
source_attachment = self.get_attachment("SOURCE", release_details)
101-
if source_attachment:
102-
rel_item["SourceFile"] = source_attachment.get("filename", "")
103-
rel_item["SourceFileHash"] = source_attachment.get("sha1", "")
104-
105-
binary_attachment = self.get_attachment("BINARY", release_details)
106-
if binary_attachment:
107-
rel_item["BinaryFile"] = binary_attachment.get("filename", "")
108-
rel_item["BinaryFileHash"] = binary_attachment.get("sha1", "")
79+
purl = self.get_external_id("purl", release_details)
80+
81+
if purl:
82+
rel_item = Component(name=release["name"], version=release["version"], purl=purl, bom_ref=purl)
83+
else:
84+
rel_item = Component(name=release["name"], version=release["version"])
85+
86+
for key, property in (("clearingState", CycloneDxSupport.CDX_PROP_CLEARING_STATE),
87+
("mainlineState", CycloneDxSupport.CDX_PROP_REL_STATE)):
88+
if key in release_details and release_details[key]:
89+
CycloneDxSupport.set_property(rel_item, property, release_details[key])
90+
91+
if "languages" in release_details and release_details["languages"]:
92+
languages = self.list_to_string(release_details["languages"])
93+
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_LANGUAGE, languages)
94+
95+
for key, comment in (("sourceCodeDownloadurl", CaPyCliBom.SOURCE_URL_COMMENT),
96+
("binaryDownloadurl", CaPyCliBom.BINARY_URL_COMMENT)):
97+
if key in release_details and release_details[key]:
98+
# add hash from attachment (see below) also here if same filename?
99+
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
100+
comment, release_details[key])
101+
102+
if "repository" in release_details and "url" in release_details["repository"]:
103+
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.VCS, comment=None,
104+
value=release_details["repository"]["url"])
105+
106+
for at_type, comment in (("SOURCE", CaPyCliBom.SOURCE_FILE_COMMENT),
107+
("BINARY", CaPyCliBom.BINARY_FILE_COMMENT)):
108+
attachments = self.get_attachments((at_type, at_type + "_SELF"), release_details)
109+
for attachment in attachments:
110+
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
111+
comment, attachment["filename"],
112+
HashAlgorithm.SHA_1, attachment.get("sha1"))
109113

110114
except sw360.SW360Error as swex:
111115
print_red(" ERROR: unable to access project:" + repr(swex))
112116
sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360)
113117

118+
state = self.get_clearing_state(project, href)
119+
if state:
120+
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_PROJ_STATE, state)
121+
122+
sw360_id = self.client.get_id_from_href(href)
123+
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_SW360ID, sw360_id)
124+
125+
CycloneDxSupport.set_property(
126+
rel_item,
127+
CycloneDxSupport.CDX_PROP_SW360_URL,
128+
self.sw360_url
129+
+ "group/guest/components/-/component/release/detailRelease/"
130+
+ sw360_id)
131+
114132
bom.append(rel_item)
115133

116134
# sub-projects are not handled at the moment
@@ -126,12 +144,7 @@ def create_project_cdx_bom(self, project_id) -> Bom:
126144

127145
print_text(" Project name: " + project["name"] + ", " + project["version"])
128146

129-
bom = self.create_project_bom(project)
130-
131-
cdx_components = []
132-
for item in bom:
133-
cx_comp = LegacySupport.legacy_component_to_cdx(item)
134-
cdx_components.append(cx_comp)
147+
cdx_components = self.create_project_bom(project)
135148

136149
creator = SbomCreator()
137150
sbom = creator.create(cdx_components, addlicense=True, addprofile=True, addtools=True,

tests/test_create_bom.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ def test_project_by_id(self):
158158
release = self.get_release_cli_for_test()
159159
# use a specific purl
160160
release["externalIds"]["package-url"] = "pkg:deb/debian/cli-support@1.3-1"
161+
# add a SOURCE_SELF attachment
162+
release["_embedded"]["sw360:attachments"].append({
163+
"filename": "clipython-repacked-for-fun.zip",
164+
"sha1": "face4b90d134e2a2bcf9464c50ea086f849a9b82",
165+
"attachmentType": "SOURCE_SELF",
166+
"_links": {
167+
"self": {
168+
"href": "https://my.server.com/resource/api/attachments/r002a002"
169+
}
170+
}
171+
})
161172
responses.add(
162173
responses.GET,
163174
url=self.MYURL + "resource/api/releases/r002",
@@ -177,7 +188,7 @@ def test_project_by_id(self):
177188
self.assertEqual(ext_refs_src_url[0].type, ExternalReferenceType.DISTRIBUTION)
178189

179190
ext_refs_src_file = [e for e in cx_comp.external_references if e.comment == CaPyCliBom.SOURCE_FILE_COMMENT]
180-
self.assertEqual(len(ext_refs_src_file), 1)
191+
self.assertEqual(len(ext_refs_src_file), 2)
181192
self.assertEqual(ext_refs_src_file[0].url, release["_embedded"]["sw360:attachments"][0]["filename"])
182193
self.assertEqual(ext_refs_src_file[0].type, ExternalReferenceType.DISTRIBUTION)
183194
self.assertEqual(ext_refs_src_file[0].hashes[0].alg, "SHA-1")

0 commit comments

Comments
 (0)