Skip to content

Commit

Permalink
feat(project CreateBom): write control file with attachment details
Browse files Browse the repository at this point in the history
This can be used to check meta data of attachments before feeding the
list into "bom downloadAttachments".
  • Loading branch information
gernot-h committed Aug 9, 2023
1 parent 51dc67a commit a3653a4
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 25 deletions.
7 changes: 7 additions & 0 deletions capycli/main/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ def register_options(self):
help="create an mapping overview JSON file",
)

self.parser.add_argument(
"-ct",
"--controlfile",
dest="controlfile",
help="control file for \"bom DownloadAttachments\" and \"project CreateReadme\"",
)

self.parser.add_argument(
"-mr",
"--mapresult",
Expand Down
46 changes: 37 additions & 9 deletions capycli/project/create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import logging
import sys
import json
from typing import Tuple, Dict

import sw360
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
Expand All @@ -17,6 +19,7 @@
import capycli.common.script_base
from capycli import get_logger
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator
from capycli.common.script_support import ScriptSupport
from capycli.common.print import print_red, print_text, print_yellow
from capycli.main.result_codes import ResultCode

Expand All @@ -42,8 +45,9 @@ def get_clearing_state(self, proj, href) -> str:

return None

def create_project_bom(self, project) -> list:
def create_project_bom(self, project, create_controlfile) -> list:
bom = []
details = []

releases = project["_embedded"].get("sw360:releases", [])
releases.sort(key=lambda s: s["name"].lower())
Expand Down Expand Up @@ -84,7 +88,6 @@ def create_project_bom(self, project) -> list:
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"])

attachments = self.get_release_attachments(release_details)
for attachment in attachments:
at_type = attachment["attachmentType"]
Expand All @@ -95,8 +98,23 @@ def create_project_bom(self, project) -> list:
ext_ref_type = ExternalReferenceType.DISTRIBUTION
else:
ext_ref_type = ExternalReferenceType.OTHER
comment += (", sw360Id: "
+ self.client.get_id_from_href(attachment["_links"]["self"]["href"]))
if create_controlfile:
at_data = self.client.get_attachment_by_url(attachment["_links"]["self"]["href"])

at_details = {
"ComponentName": " ".join((release["name"], release["version"])),
"Sw360Id": sw360_id,
"Sw360AttachmentId": self.client.get_id_from_href(attachment["_links"]["self"]["href"])}
for key in ("createdBy", "createdTeam", "createdOn", "createdComment", "checkStatus",
"checkedBy", "checkedTeam", "checkedOn", "checkedComment"):
if key in at_data and at_data[key]:
at_details[key[0].upper() + key[1:]] = at_data[key]

if at_type == "COMPONENT_LICENSE_INFO_XML":
at_details["CliFile"] = attachment["filename"]
elif at_type == "CLEARING_REPORT":
at_details["ReportFile"] = attachment["filename"]
details.append(at_details)
CycloneDxSupport.set_ext_ref(rel_item, ext_ref_type,
comment, attachment["filename"],
HashAlgorithm.SHA_1, attachment.get("sha1"))
Expand All @@ -120,9 +138,9 @@ def create_project_bom(self, project) -> list:

# sub-projects are not handled at the moment

return bom
return bom, details

def create_project_cdx_bom(self, project_id) -> Bom:
def create_project_cdx_bom(self, project_id, create_controlfile) -> Tuple[Bom, Dict]:
try:
project = self.client.get_project(project_id)
except sw360.sw360_api.SW360Error as swex:
Expand All @@ -131,14 +149,19 @@ def create_project_cdx_bom(self, project_id) -> Bom:

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

cdx_components = self.create_project_bom(project)
cdx_components, control_components = self.create_project_bom(project, create_controlfile)

creator = SbomCreator()
sbom = creator.create(cdx_components, addlicense=True, addprofile=True, addtools=True,
name=project.get("name"), version=project.get("version"),
description=project.get("description"), addprojectdependencies=True)

return sbom
controlfile = {
"ProjectName": ScriptSupport.get_full_name_from_dict(project, "name", "version"),
"Components": control_components
}

return sbom, controlfile

def show_command_help(self):
print("\nusage: CaPyCli project createbom [options]")
Expand All @@ -151,6 +174,7 @@ def show_command_help(self):
-name name of the project, component or release
-version version of the project, component or release
-o OUTPUTFILE output file to write to
-ct CONTROLFILE write control file for "bom DownloadAttachments" and "project CreateReadme"
""")

print()
Expand Down Expand Up @@ -197,7 +221,11 @@ def run(self, args):
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if pid:
bom = self.create_project_cdx_bom(pid)
bom, controlfile = self.create_project_cdx_bom(pid, args.controlfile)
CaPyCliBom.write_sbom(bom, args.outputfile)

if args.controlfile:
with open(args.controlfile, "w") as outfile:
json.dump(controlfile, outfile, indent=2)
else:
print_yellow(" No matching project found")
4 changes: 2 additions & 2 deletions tests/fixtures/sbom_for_download.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
},
{
"url": "CLIXML_certifi-2022.12.7.xml",
"comment": "component license information (local copy), sw360Id: 794446",
"comment": "component license information (local copy)",
"type": "other",
"hashes": [
{
Expand All @@ -102,7 +102,7 @@
},
{
"url": "certifi-2022.12.7_clearing_report.docx",
"comment": "clearing report (local copy), sw360Id: 63b368",
"comment": "clearing report (local copy)",
"type": "other",
"hashes": [
{
Expand Down
158 changes: 144 additions & 14 deletions tests/test_create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def test_project_not_found(self) -> None:
args.verbose = True
args.id = "34ef5c5452014c52aa9ce4bc180624d8"
args.outputfile = self.OUTPUTFILE
args.controlfile = None

self.add_login_response()

Expand All @@ -126,19 +127,12 @@ def test_project_not_found(self) -> None:
except SystemExit as ex:
self.assertEqual(ResultCode.RESULT_ERROR_ACCESSING_SW360, ex.code)

@responses.activate
def test_project_by_id(self):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)

def add_project_releases_responses(self):
# the project
project = self.get_project_for_test()
responses.add(
responses.GET,
url=self.MYURL + "resource/api/projects/p001",
json=project,
json=self.get_project_for_test(),
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
Expand All @@ -165,7 +159,7 @@ def test_project_by_id(self):
"attachmentType": "SOURCE_SELF",
"_links": {
"self": {
"href": "https://my.server.com/resource/api/attachments/r002a002"
"href": "https://my.server.com/resource/api/attachments/r002a003"
}
}
})
Expand All @@ -175,7 +169,7 @@ def test_project_by_id(self):
"attachmentType": "CLEARING_REPORT",
"_links": {
"self": {
"href": "https://my.server.com/resource/api/attachments/r002a003"
"href": "https://my.server.com/resource/api/attachments/r002a004"
}
}
})
Expand All @@ -188,8 +182,19 @@ def test_project_by_id(self):
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
return release

cdx_bom = sut.create_project_cdx_bom("p001")
@responses.activate
def test_project_by_id(self):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)

release = self.add_project_releases_responses()
project = self.get_project_for_test()

cdx_bom, _ = sut.create_project_cdx_bom("p001", create_controlfile=False)
cx_comp = cdx_bom.components[0]
self.assertEqual(cx_comp.purl, release["externalIds"]["package-url"])

Expand All @@ -210,15 +215,15 @@ def test_project_by_id(self):
self.assertEqual(len(ext_refs), 1)
self.assertEqual(ext_refs[0].url, release["_embedded"]["sw360:attachments"][1]["filename"])
self.assertEqual(ext_refs[0].type, ExternalReferenceType.OTHER)
self.assertEqual(ext_refs[0].comment, CaPyCliBom.CLI_FILE_COMMENT + ", sw360Id: r002a002")
self.assertEqual(ext_refs[0].comment, CaPyCliBom.CLI_FILE_COMMENT)
self.assertEqual(ext_refs[0].hashes[0].alg, "SHA-1")
self.assertEqual(ext_refs[0].hashes[0].content, release["_embedded"]["sw360:attachments"][1]["sha1"])

ext_refs = [e for e in cx_comp.external_references
if e.comment and e.comment.startswith(CaPyCliBom.CRT_FILE_COMMENT)]
self.assertEqual(len(ext_refs), 1)
self.assertEqual(ext_refs[0].url, release["_embedded"]["sw360:attachments"][3]["filename"])
self.assertEqual(ext_refs[0].comment, CaPyCliBom.CRT_FILE_COMMENT + ", sw360Id: r002a003")
self.assertEqual(ext_refs[0].comment, CaPyCliBom.CRT_FILE_COMMENT)
self.assertEqual(ext_refs[0].type, ExternalReferenceType.OTHER)
self.assertEqual(ext_refs[0].hashes[0].alg, "SHA-1")
self.assertEqual(ext_refs[0].hashes[0].content, release["_embedded"]["sw360:attachments"][3]["sha1"])
Expand All @@ -231,6 +236,71 @@ def test_project_by_id(self):
self.assertEqual(cdx_bom.metadata.component.version, project["version"])
self.assertEqual(cdx_bom.metadata.component.description, project["description"])

@responses.activate
def test_project_by_id_controlfile(self):
sut = CreateBom()
self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)

self.add_project_releases()

# attachment info
responses.add(
method=responses.GET,
url=self.MYURL + "resource/api/attachments/r001a001",
body="""
{
"filename": "CLIXML_wheel-0.38.4.xml",
"sha1": "ccd9f1ed2f59c46ff3f0139c05bfd76f83fd9851",
"attachmentType": "COMPONENT_LICENSE_INFO_XML"
}""",
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
responses.add(
method=responses.GET,
url=self.MYURL + "resource/api/attachments/r002a002",
body="""
{
"filename": "CLIXML_clipython-1.3.0.xml",
"sha1": "dd4c38387c6811dba67d837af7742d84e61e20de",
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
"checkedBy": "user2@siemens.com",
"checkStatus": "ACCEPTED",
"createdBy": "user1@siemens.com"
}""",
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
responses.add(
method=responses.GET,
url=self.MYURL + "resource/api/attachments/r002a004",
body="""
{
"filename": "clipython-1.3.0.docx",
"sha1": "f0d8f2ddd017bdeaecbaec72ff76a6c0a045ec66",
"attachmentType": "CLEARING_REPORT"
}""",
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)

_, controlfile = sut.create_project_cdx_bom("p001", create_controlfile=True)
self.assertEqual(controlfile['ProjectName'], 'CaPyCLI, 1.9.0')
self.assertEqual(controlfile['Components'][0]['ComponentName'], 'cli-support 1.3')
self.assertEqual(controlfile['Components'][0]['Sw360Id'], 'r002')
self.assertEqual(controlfile['Components'][0]['Sw360AttachmentId'], 'r002a002')
self.assertEqual(controlfile['Components'][0]['CliFile'], 'CLIXML_clipython-1.3.0.xml')
self.assertEqual(controlfile['Components'][0]['CheckedBy'], 'user2@siemens.com')
self.assertEqual(controlfile['Components'][0]['CheckStatus'], 'ACCEPTED')
self.assertEqual(controlfile['Components'][0]['CreatedBy'], 'user1@siemens.com')

self.assertEqual(controlfile['Components'][1]['ReportFile'], 'clipython-1.3.0.docx')

@responses.activate
def test_project_show_by_name(self):
sut = CreateBom()
Expand All @@ -246,6 +316,7 @@ def test_project_show_by_name(self):
args.name = "CaPyCLI"
args.version = "1.9.0"
args.outputfile = self.OUTPUTFILE
args.controlfile = None

self.add_login_response()

Expand Down Expand Up @@ -329,6 +400,65 @@ def test_project_show_by_name(self):

self.delete_file(self.OUTPUTFILE)

@responses.activate
def test_create_project_bom_release_error(self):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)

responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r001",
status=404,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r002",
json=self.get_release_cli_for_test(),
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
with self.assertRaises(SystemExit):
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=False)

@responses.activate
def test_create_project_bom_controlfile_attachment_error(self):
sut = CreateBom()

self.add_login_response()
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)

responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r001",
json=self.get_release_wheel_for_test(),
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
responses.add(
responses.GET,
url=self.MYURL + "resource/api/releases/r002",
json=self.get_release_cli_for_test(),
status=200,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)
responses.add(
method=responses.GET,
url=self.MYURL + "resource/api/attachments/r002a002",
status=404,
content_type="application/json",
adding_headers={"Authorization": "Token " + self.MYTOKEN},
)

with self.assertRaises(SystemExit):
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=True)


if __name__ == "__main__":
APP = TestCreateBom()
Expand Down

0 comments on commit a3653a4

Please sign in to comment.