Skip to content

Commit

Permalink
Merge pull request #32 from sw360/feat-multiple-sources
Browse files Browse the repository at this point in the history
project CreateBom: add multiple and self-made sources to SBOM
  • Loading branch information
tngraf authored Jul 30, 2023
2 parents e1b56fa + 13af321 commit a08cbc2
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 58 deletions.
2 changes: 1 addition & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* 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` 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` add SW360 source and binary attachments as external reference to SBOM.
* `project createbom` adds SW360 project name, version and description to SBOM.

## 2.0.0 (2023-06-02)
Expand Down
28 changes: 22 additions & 6 deletions capycli/common/capycli_bom_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,11 @@ def get_property(comp: Component, name: str) -> Any:

return None

@staticmethod
def set_property(comp: Component, name: str, value: str) -> None:
"""Sets the property with the given name."""
comp.properties.add(Property(name=name, value=value))

@staticmethod
def update_or_set_property(comp: Component, name: str, value: str) -> None:
"""Returns the property with the given name."""
Expand All @@ -310,7 +315,7 @@ def update_or_set_property(comp: Component, name: str, value: str) -> None:
if prop:
prop.value = value
else:
comp.properties.add(Property(name=name, value=value))
CycloneDxSupport.set_property(comp, name, value)

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

return None

@staticmethod
def set_ext_ref(comp: Component, type: ExternalReferenceType, comment: str, value: str,
hash_algo: str = None, hash: str = None) -> None:
ext_ref = ExternalReference(
reference_type=type,
url=value,
comment=comment)

if hash_algo and hash:
ext_ref.hashes.add(HashType(
algorithm=HashAlgorithm.SHA_1,
hash_value=hash))

comp.external_references.add(ext_ref)

@staticmethod
def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment: str, value: str) -> None:
ext_ref = None
Expand All @@ -348,11 +368,7 @@ def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment:
if ext_ref:
ext_ref.url = value
else:
ext_ref = ExternalReference(
reference_type=type,
url=value,
comment=comment)
comp.external_references.add(ext_ref)
CycloneDxSupport.set_ext_ref(comp, type, comment, value)

@staticmethod
def get_ext_ref_by_comment(comp: Component, comment: str) -> Any:
Expand Down
113 changes: 63 additions & 50 deletions capycli/project/create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
import logging
import sys

from typing import List, Tuple

import sw360
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component

import capycli.common.script_base
from capycli import get_logger
from capycli.bom.legacy import LegacySupport
from capycli.common.capycli_bom_support import CaPyCliBom, SbomCreator
from capycli.common.capycli_bom_support import CaPyCliBom, SbomCreator, CycloneDxSupport

from capycli.common.print import print_red, print_text, print_yellow
from capycli.main.result_codes import ResultCode

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

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

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

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

found = []
attachments = release_details["_embedded"]["sw360:attachments"]
for attachment in attachments:
if attachment["attachmentType"] == att_type:
return attachment
if attachment["attachmentType"] in att_types:
found.append(attachment)

return None
return found

def get_clearing_state(self, proj, href) -> str:
"""Returns the clearing state of the given component/release"""
Expand All @@ -64,53 +69,66 @@ def create_project_bom(self, project) -> list:
for release in releases:
print_text(" ", release["name"], release["version"])
href = release["_links"]["self"]["href"]
state = self.get_clearing_state(project, href)

rel_item = {}
rel_item["Name"] = release["name"]
rel_item["Version"] = release["version"]
rel_item["ProjectClearingState"] = state
rel_item["Id"] = self.client.get_id_from_href(href)
rel_item["Sw360Id"] = rel_item["Id"]
rel_item["Url"] = (
self.sw360_url
+ "group/guest/components/-/component/release/detailRelease/"
+ self.client.get_id_from_href(href))

try:
release_details = self.client.get_release_by_url(href)
# capycli.common.json_support.print_json(release_details)
rel_item["ClearingState"] = release_details["clearingState"]
rel_item["ReleaseMainlineState"] = release_details.get("mainlineState", "")

rel_item["Language"] = self.list_to_string(release_details.get("languages", ""))
rel_item["SourceFileUrl"] = release_details.get("sourceCodeDownloadurl", "")
rel_item["BinaryFileUrl"] = release_details.get("binaryDownloadurl", "")

rel_item["RepositoryId"] = self.get_external_id("package-url", release_details)
if not rel_item["RepositoryId"]:
purl = self.get_external_id("package-url", release_details)
if not purl:
# try another id name
rel_item["RepositoryId"] = self.get_external_id("purl", release_details)
if rel_item["RepositoryId"]:
rel_item["RepositoryType"] = "package-url"

if "repository" in release_details:
rel_item["RepositoryUrl"] = release_details["repository"].get("url", "")

source_attachment = self.get_attachment("SOURCE", release_details)
if source_attachment:
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["BinaryFileHash"] = binary_attachment.get("sha1", "")
purl = self.get_external_id("purl", release_details)

if purl:
rel_item = Component(name=release["name"], version=release["version"], purl=purl, bom_ref=purl)
else:
rel_item = Component(name=release["name"], version=release["version"])

for key, property in (("clearingState", CycloneDxSupport.CDX_PROP_CLEARING_STATE),
("mainlineState", CycloneDxSupport.CDX_PROP_REL_STATE)):
if key in release_details and release_details[key]:
CycloneDxSupport.set_property(rel_item, property, release_details[key])

if "languages" in release_details and release_details["languages"]:
languages = self.list_to_string(release_details["languages"])
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_LANGUAGE, languages)

for key, comment in (("sourceCodeDownloadurl", CaPyCliBom.SOURCE_URL_COMMENT),
("binaryDownloadurl", CaPyCliBom.BINARY_URL_COMMENT)):
if key in release_details and release_details[key]:
# add hash from attachment (see below) also here if same filename?
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
comment, release_details[key])

if "repository" in release_details and "url" in release_details["repository"]:
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.VCS, comment=None,
value=release_details["repository"]["url"])

for at_type, comment in (("SOURCE", CaPyCliBom.SOURCE_FILE_COMMENT),
("BINARY", CaPyCliBom.BINARY_FILE_COMMENT)):
attachments = self.get_attachments((at_type, at_type + "_SELF"), release_details)
for attachment in attachments:
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
comment, attachment["filename"],
HashAlgorithm.SHA_1, attachment.get("sha1"))

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

state = self.get_clearing_state(project, href)
if state:
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_PROJ_STATE, state)

sw360_id = self.client.get_id_from_href(href)
CycloneDxSupport.set_property(rel_item, CycloneDxSupport.CDX_PROP_SW360ID, sw360_id)

CycloneDxSupport.set_property(
rel_item,
CycloneDxSupport.CDX_PROP_SW360_URL,
self.sw360_url
+ "group/guest/components/-/component/release/detailRelease/"
+ sw360_id)

bom.append(rel_item)

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

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

bom = self.create_project_bom(project)

cdx_components = []
for item in bom:
cx_comp = LegacySupport.legacy_component_to_cdx(item)
cdx_components.append(cx_comp)
cdx_components = self.create_project_bom(project)

creator = SbomCreator()
sbom = creator.create(cdx_components, addlicense=True, addprofile=True, addtools=True,
Expand Down
13 changes: 12 additions & 1 deletion tests/test_create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ def test_project_by_id(self):
release = self.get_release_cli_for_test()
# use a specific purl
release["externalIds"]["package-url"] = "pkg:deb/debian/[email protected]"
# add a SOURCE_SELF attachment
release["_embedded"]["sw360:attachments"].append({
"filename": "clipython-repacked-for-fun.zip",
"sha1": "face4b90d134e2a2bcf9464c50ea086f849a9b82",
"attachmentType": "SOURCE_SELF",
"_links": {
"self": {
"href": "https://my.server.com/resource/api/attachments/r002a002"
}
}
})
responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r002",
Expand All @@ -177,7 +188,7 @@ def test_project_by_id(self):
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(len(ext_refs_src_file), 2)
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")
Expand Down

0 comments on commit a08cbc2

Please sign in to comment.