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 21, 2023
1 parent e39df2d commit 9700964
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 @@ -210,6 +210,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 @@ -18,6 +20,7 @@
from capycli import get_logger
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator
from capycli.common.purl_utils import PurlUtils
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 @@ -43,8 +46,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 @@ -91,7 +95,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 @@ -102,8 +105,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 @@ -127,9 +145,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 @@ -138,14 +156,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 @@ -158,6 +181,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 @@ -204,7 +228,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 @@ -102,6 +102,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 Down Expand Up @@ -155,19 +156,12 @@ def test_create_bom_multiple_purls(self, capsys):
assert "Multiple purls added" in captured.out
assert cdx_components[0].purl == "pkg:deb/debian/[email protected] pkg:pypi/[email protected]"

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

self.add_login_response()
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.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 @@ -194,7 +188,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 @@ -204,7 +198,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 @@ -217,8 +211,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=TestBasePytest.MYTOKEN, url=TestBasePytest.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]
assert cx_comp.purl == release["externalIds"]["package-url"]

Expand All @@ -239,15 +244,15 @@ def test_project_by_id(self):
assert len(ext_refs) == 1
assert ext_refs[0].url == release["_embedded"]["sw360:attachments"][1]["filename"]
assert ext_refs[0].type == ExternalReferenceType.OTHER
assert ext_refs[0].comment, CaPyCliBom.CLI_FILE_COMMENT + " == sw360Id: r002a002"
assert ext_refs[0].comment == CaPyCliBom.CLI_FILE_COMMENT
assert ext_refs[0].hashes[0].alg == "SHA-1"
assert 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)]
assert len(ext_refs) == 1
assert ext_refs[0].url == release["_embedded"]["sw360:attachments"][3]["filename"]
assert ext_refs[0].comment, CaPyCliBom.CRT_FILE_COMMENT + " == sw360Id: r002a003"
assert ext_refs[0].comment == CaPyCliBom.CRT_FILE_COMMENT
assert ext_refs[0].type == ExternalReferenceType.OTHER
assert ext_refs[0].hashes[0].alg == "SHA-1"
assert ext_refs[0].hashes[0].content == release["_embedded"]["sw360:attachments"][3]["sha1"]
Expand All @@ -260,6 +265,71 @@ def test_project_by_id(self):
assert cdx_bom.metadata.component.version == project["version"]
assert 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=TestBasePytest.MYTOKEN, url=TestBasePytest.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": "[email protected]",
"checkStatus": "ACCEPTED",
"createdBy": "[email protected]"
}""",
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)
assert controlfile['ProjectName'] == 'CaPyCLI, 1.9.0'
assert controlfile['Components'][0]['ComponentName'] == 'cli-support 1.3'
assert controlfile['Components'][0]['Sw360Id'] == 'r002'
assert controlfile['Components'][0]['Sw360AttachmentId'] == 'r002a002'
assert controlfile['Components'][0]['CliFile'] == 'CLIXML_clipython-1.3.0.xml'
assert controlfile['Components'][0]['CheckedBy'] == '[email protected]'
assert controlfile['Components'][0]['CheckStatus'] == 'ACCEPTED'
assert controlfile['Components'][0]['CreatedBy'] == '[email protected]'

assert controlfile['Components'][1]['ReportFile'] == 'clipython-1.3.0.docx'

@responses.activate
def test_project_show_by_name(self):
sut = CreateBom()
Expand All @@ -275,6 +345,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 @@ -358,6 +429,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=TestBasePytest.MYTOKEN, url=TestBasePytest.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 pytest.raises(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=TestBasePytest.MYTOKEN, url=TestBasePytest.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 pytest.raises(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 9700964

Please sign in to comment.