Skip to content

Commit a3653a4

Browse files
committed
feat(project CreateBom): write control file with attachment details
This can be used to check meta data of attachments before feeding the list into "bom downloadAttachments".
1 parent 51dc67a commit a3653a4

4 files changed

Lines changed: 190 additions & 25 deletions

File tree

capycli/main/options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ def register_options(self):
208208
help="create an mapping overview JSON file",
209209
)
210210

211+
self.parser.add_argument(
212+
"-ct",
213+
"--controlfile",
214+
dest="controlfile",
215+
help="control file for \"bom DownloadAttachments\" and \"project CreateReadme\"",
216+
)
217+
211218
self.parser.add_argument(
212219
"-mr",
213220
"--mapresult",

capycli/project/create_bom.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import logging
1010
import sys
11+
import json
12+
from typing import Tuple, Dict
1113

1214
import sw360
1315
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
@@ -17,6 +19,7 @@
1719
import capycli.common.script_base
1820
from capycli import get_logger
1921
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator
22+
from capycli.common.script_support import ScriptSupport
2023
from capycli.common.print import print_red, print_text, print_yellow
2124
from capycli.main.result_codes import ResultCode
2225

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

4346
return None
4447

45-
def create_project_bom(self, project) -> list:
48+
def create_project_bom(self, project, create_controlfile) -> list:
4649
bom = []
50+
details = []
4751

4852
releases = project["_embedded"].get("sw360:releases", [])
4953
releases.sort(key=lambda s: s["name"].lower())
@@ -84,7 +88,6 @@ def create_project_bom(self, project) -> list:
8488
if "repository" in release_details and "url" in release_details["repository"]:
8589
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.VCS, comment=None,
8690
value=release_details["repository"]["url"])
87-
8891
attachments = self.get_release_attachments(release_details)
8992
for attachment in attachments:
9093
at_type = attachment["attachmentType"]
@@ -95,8 +98,23 @@ def create_project_bom(self, project) -> list:
9598
ext_ref_type = ExternalReferenceType.DISTRIBUTION
9699
else:
97100
ext_ref_type = ExternalReferenceType.OTHER
98-
comment += (", sw360Id: "
99-
+ self.client.get_id_from_href(attachment["_links"]["self"]["href"]))
101+
if create_controlfile:
102+
at_data = self.client.get_attachment_by_url(attachment["_links"]["self"]["href"])
103+
104+
at_details = {
105+
"ComponentName": " ".join((release["name"], release["version"])),
106+
"Sw360Id": sw360_id,
107+
"Sw360AttachmentId": self.client.get_id_from_href(attachment["_links"]["self"]["href"])}
108+
for key in ("createdBy", "createdTeam", "createdOn", "createdComment", "checkStatus",
109+
"checkedBy", "checkedTeam", "checkedOn", "checkedComment"):
110+
if key in at_data and at_data[key]:
111+
at_details[key[0].upper() + key[1:]] = at_data[key]
112+
113+
if at_type == "COMPONENT_LICENSE_INFO_XML":
114+
at_details["CliFile"] = attachment["filename"]
115+
elif at_type == "CLEARING_REPORT":
116+
at_details["ReportFile"] = attachment["filename"]
117+
details.append(at_details)
100118
CycloneDxSupport.set_ext_ref(rel_item, ext_ref_type,
101119
comment, attachment["filename"],
102120
HashAlgorithm.SHA_1, attachment.get("sha1"))
@@ -120,9 +138,9 @@ def create_project_bom(self, project) -> list:
120138

121139
# sub-projects are not handled at the moment
122140

123-
return bom
141+
return bom, details
124142

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

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

134-
cdx_components = self.create_project_bom(project)
152+
cdx_components, control_components = self.create_project_bom(project, create_controlfile)
135153

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

141-
return sbom
159+
controlfile = {
160+
"ProjectName": ScriptSupport.get_full_name_from_dict(project, "name", "version"),
161+
"Components": control_components
162+
}
163+
164+
return sbom, controlfile
142165

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

156180
print()
@@ -197,7 +221,11 @@ def run(self, args):
197221
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
198222

199223
if pid:
200-
bom = self.create_project_cdx_bom(pid)
224+
bom, controlfile = self.create_project_cdx_bom(pid, args.controlfile)
201225
CaPyCliBom.write_sbom(bom, args.outputfile)
226+
227+
if args.controlfile:
228+
with open(args.controlfile, "w") as outfile:
229+
json.dump(controlfile, outfile, indent=2)
202230
else:
203231
print_yellow(" No matching project found")

tests/fixtures/sbom_for_download.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
},
9292
{
9393
"url": "CLIXML_certifi-2022.12.7.xml",
94-
"comment": "component license information (local copy), sw360Id: 794446",
94+
"comment": "component license information (local copy)",
9595
"type": "other",
9696
"hashes": [
9797
{
@@ -102,7 +102,7 @@
102102
},
103103
{
104104
"url": "certifi-2022.12.7_clearing_report.docx",
105-
"comment": "clearing report (local copy), sw360Id: 63b368",
105+
"comment": "clearing report (local copy)",
106106
"type": "other",
107107
"hashes": [
108108
{

tests/test_create_bom.py

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def test_project_not_found(self) -> None:
107107
args.verbose = True
108108
args.id = "34ef5c5452014c52aa9ce4bc180624d8"
109109
args.outputfile = self.OUTPUTFILE
110+
args.controlfile = None
110111

111112
self.add_login_response()
112113

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

129-
@responses.activate
130-
def test_project_by_id(self):
131-
sut = CreateBom()
132-
133-
self.add_login_response()
134-
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
135-
130+
def add_project_releases_responses(self):
136131
# the project
137-
project = self.get_project_for_test()
138132
responses.add(
139133
responses.GET,
140134
url=self.MYURL + "resource/api/projects/p001",
141-
json=project,
135+
json=self.get_project_for_test(),
142136
status=200,
143137
content_type="application/json",
144138
adding_headers={"Authorization": "Token " + self.MYTOKEN},
@@ -165,7 +159,7 @@ def test_project_by_id(self):
165159
"attachmentType": "SOURCE_SELF",
166160
"_links": {
167161
"self": {
168-
"href": "https://my.server.com/resource/api/attachments/r002a002"
162+
"href": "https://my.server.com/resource/api/attachments/r002a003"
169163
}
170164
}
171165
})
@@ -175,7 +169,7 @@ def test_project_by_id(self):
175169
"attachmentType": "CLEARING_REPORT",
176170
"_links": {
177171
"self": {
178-
"href": "https://my.server.com/resource/api/attachments/r002a003"
172+
"href": "https://my.server.com/resource/api/attachments/r002a004"
179173
}
180174
}
181175
})
@@ -188,8 +182,19 @@ def test_project_by_id(self):
188182
content_type="application/json",
189183
adding_headers={"Authorization": "Token " + self.MYTOKEN},
190184
)
185+
return release
191186

192-
cdx_bom = sut.create_project_cdx_bom("p001")
187+
@responses.activate
188+
def test_project_by_id(self):
189+
sut = CreateBom()
190+
191+
self.add_login_response()
192+
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
193+
194+
release = self.add_project_releases_responses()
195+
project = self.get_project_for_test()
196+
197+
cdx_bom, _ = sut.create_project_cdx_bom("p001", create_controlfile=False)
193198
cx_comp = cdx_bom.components[0]
194199
self.assertEqual(cx_comp.purl, release["externalIds"]["package-url"])
195200

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

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

239+
@responses.activate
240+
def test_project_by_id_controlfile(self):
241+
sut = CreateBom()
242+
self.add_login_response()
243+
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
244+
245+
self.add_project_releases()
246+
247+
# attachment info
248+
responses.add(
249+
method=responses.GET,
250+
url=self.MYURL + "resource/api/attachments/r001a001",
251+
body="""
252+
{
253+
"filename": "CLIXML_wheel-0.38.4.xml",
254+
"sha1": "ccd9f1ed2f59c46ff3f0139c05bfd76f83fd9851",
255+
"attachmentType": "COMPONENT_LICENSE_INFO_XML"
256+
}""",
257+
status=200,
258+
content_type="application/json",
259+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
260+
)
261+
responses.add(
262+
method=responses.GET,
263+
url=self.MYURL + "resource/api/attachments/r002a002",
264+
body="""
265+
{
266+
"filename": "CLIXML_clipython-1.3.0.xml",
267+
"sha1": "dd4c38387c6811dba67d837af7742d84e61e20de",
268+
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
269+
"checkedBy": "user2@siemens.com",
270+
"checkStatus": "ACCEPTED",
271+
"createdBy": "user1@siemens.com"
272+
}""",
273+
status=200,
274+
content_type="application/json",
275+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
276+
)
277+
responses.add(
278+
method=responses.GET,
279+
url=self.MYURL + "resource/api/attachments/r002a004",
280+
body="""
281+
{
282+
"filename": "clipython-1.3.0.docx",
283+
"sha1": "f0d8f2ddd017bdeaecbaec72ff76a6c0a045ec66",
284+
"attachmentType": "CLEARING_REPORT"
285+
286+
}""",
287+
status=200,
288+
content_type="application/json",
289+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
290+
)
291+
292+
_, controlfile = sut.create_project_cdx_bom("p001", create_controlfile=True)
293+
self.assertEqual(controlfile['ProjectName'], 'CaPyCLI, 1.9.0')
294+
self.assertEqual(controlfile['Components'][0]['ComponentName'], 'cli-support 1.3')
295+
self.assertEqual(controlfile['Components'][0]['Sw360Id'], 'r002')
296+
self.assertEqual(controlfile['Components'][0]['Sw360AttachmentId'], 'r002a002')
297+
self.assertEqual(controlfile['Components'][0]['CliFile'], 'CLIXML_clipython-1.3.0.xml')
298+
self.assertEqual(controlfile['Components'][0]['CheckedBy'], 'user2@siemens.com')
299+
self.assertEqual(controlfile['Components'][0]['CheckStatus'], 'ACCEPTED')
300+
self.assertEqual(controlfile['Components'][0]['CreatedBy'], 'user1@siemens.com')
301+
302+
self.assertEqual(controlfile['Components'][1]['ReportFile'], 'clipython-1.3.0.docx')
303+
234304
@responses.activate
235305
def test_project_show_by_name(self):
236306
sut = CreateBom()
@@ -246,6 +316,7 @@ def test_project_show_by_name(self):
246316
args.name = "CaPyCLI"
247317
args.version = "1.9.0"
248318
args.outputfile = self.OUTPUTFILE
319+
args.controlfile = None
249320

250321
self.add_login_response()
251322

@@ -329,6 +400,65 @@ def test_project_show_by_name(self):
329400

330401
self.delete_file(self.OUTPUTFILE)
331402

403+
@responses.activate
404+
def test_create_project_bom_release_error(self):
405+
sut = CreateBom()
406+
407+
self.add_login_response()
408+
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
409+
410+
responses.add(
411+
responses.GET,
412+
url=self.MYURL + "resource/api/releases/r001",
413+
status=404,
414+
content_type="application/json",
415+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
416+
)
417+
responses.add(
418+
responses.GET,
419+
url=self.MYURL + "resource/api/releases/r002",
420+
json=self.get_release_cli_for_test(),
421+
status=200,
422+
content_type="application/json",
423+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
424+
)
425+
with self.assertRaises(SystemExit):
426+
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=False)
427+
428+
@responses.activate
429+
def test_create_project_bom_controlfile_attachment_error(self):
430+
sut = CreateBom()
431+
432+
self.add_login_response()
433+
sut.login(token=TestBase.MYTOKEN, url=TestBase.MYURL)
434+
435+
responses.add(
436+
responses.GET,
437+
url=self.MYURL + "resource/api/releases/r001",
438+
json=self.get_release_wheel_for_test(),
439+
status=200,
440+
content_type="application/json",
441+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
442+
)
443+
responses.add(
444+
responses.GET,
445+
url=self.MYURL + "resource/api/releases/r002",
446+
json=self.get_release_cli_for_test(),
447+
status=200,
448+
content_type="application/json",
449+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
450+
)
451+
responses.add(
452+
method=responses.GET,
453+
url=self.MYURL + "resource/api/attachments/r002a002",
454+
status=404,
455+
content_type="application/json",
456+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
457+
)
458+
459+
with self.assertRaises(SystemExit):
460+
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=True)
461+
332462

333463
if __name__ == "__main__":
334464
APP = TestCreateBom()

0 commit comments

Comments
 (0)